diff --git a/.github/workflows/embedded-replay-profile.yml b/.github/workflows/embedded-replay-profile.yml index 42768a9de..659ecc3ed 100644 --- a/.github/workflows/embedded-replay-profile.yml +++ b/.github/workflows/embedded-replay-profile.yml @@ -1,19 +1,51 @@ name: Published embedded replay profile +run-name: ${{ inputs.execution == 'continuation' && format('Embedded continuation {0} events / {1} workers / {2} runs / observers {3}', inputs.events, inputs.workers, inputs.runs, inputs.observers) || 'Published embedded replay profile' }} on: workflow_dispatch: inputs: + execution: + description: 'Published cold bridge or candidate real-queue continuation comparison' + type: choice + options: [published, continuation] + default: published history_sizes: description: 'Event targets: all (10/100/1000), small (10/100), or large (1000)' type: choice options: [all, small, large] default: all + events: + description: 'Continuation profile: events per original' + type: choice + options: ['10', '100', '1000'] + default: '10' + workers: + description: 'Continuation profile: persistent workers within 1 GiB' + type: choice + options: ['1', '4'] + default: '1' + runs: + description: 'Continuation profile: concurrent original workflows' + type: choice + options: ['1', '8'] + default: '1' + observers: + description: 'CPU/SQL/scheduling observers; compare uses 100 events, 1 worker/run with drift controls' + type: choice + options: ['on', 'off', 'compare'] + default: 'on' + repetitions: + description: 'Continuation profile: one bounded pilot or all three measured repetitions' + type: choice + options: ['all', '1'] + default: 'all' permissions: contents: read jobs: profile: + if: ${{ inputs.execution != 'continuation' }} runs-on: ubuntu-24.04 timeout-minutes: 120 strategy: @@ -147,3 +179,77 @@ jobs: if test -d benchmarks/embedded-replay/vendor; then rm -r -- benchmarks/embedded-replay/vendor fi + + continuation: + if: ${{ inputs.execution == 'continuation' }} + runs-on: ubuntu-24.04 + timeout-minutes: 135 + strategy: + fail-fast: false + matrix: + repetition: ${{ fromJSON(inputs.repetitions == '1' && '[1]' || '[1,2,3]') }} + env: + DWF_PROFILE_IMAGE: dw-continuation-profile:qualification + DWF_GROWTH_OBSERVERS: ${{ inputs.observers }} + PROFILE_EVENTS: ${{ inputs.events }} + PROFILE_WORKERS: ${{ inputs.workers }} + PROFILE_RUNS: ${{ inputs.runs }} + PROFILE_REPETITION: ${{ matrix.repetition }} + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + + - name: Record host conditions and prepare the locked source consumer + timeout-minutes: 8 + run: | + uname -srmo + lscpu | grep -E 'Architecture:|Model name:|CPU\(s\):' + docker ps --format '{{.Names}} {{.Image}}' + docker build --tag "$DWF_PROFILE_IMAGE" tests/Performance/continuation + cp tests/Performance/continuation/COMPOSER-INPUT.lock composer.lock + root_version=$(jq -r '.extra["durable-workflow"]["product-train"]' composer.json) + docker run --rm --user "$(id -u):$(id -g)" \ + --volume "$PWD:/app" --workdir /app \ + --env COMPOSER_HOME=/tmp/composer --env "COMPOSER_ROOT_VERSION=$root_version" \ + "$DWF_PROFILE_IMAGE" composer install --prefer-dist --no-interaction --no-progress + + - name: Compare fresh replay and retained originals + run: | + if [[ "$DWF_GROWTH_OBSERVERS" == compare ]]; then + [[ "$PROFILE_EVENTS/$PROFILE_WORKERS/$PROFILE_RUNS" == 100/1/1 ]] + observer_order='on off on' + if [[ "$PROFILE_REPETITION" == 2 ]]; then observer_order='off on off'; fi + observer_block=0 + observer_outcome=0 + for observer_setting in $observer_order; do + observer_block=$((observer_block + 1)) + echo "CONTINUATION_OBSERVER_BLOCK=$observer_block setting=$observer_setting" + if DWF_GROWTH_OBSERVERS="$observer_setting" \ + bash scripts/profile-embedded-continuation.sh \ + "$PROFILE_EVENTS" "$PROFILE_WORKERS" "$PROFILE_RUNS" "$PROFILE_REPETITION"; then + : + else + observer_outcome=1 + fi + done + exit "$observer_outcome" + fi + bash scripts/profile-embedded-continuation.sh \ + "$PROFILE_EVENTS" "$PROFILE_WORKERS" "$PROFILE_RUNS" "$PROFILE_REPETITION" + + - name: Record the scope and remove disposable installations + if: always() + run: | + { + echo '## Embedded continuation source comparison' + echo "Source: $GITHUB_SHA" + echo "Events: $PROFILE_EVENTS; workers: $PROFILE_WORKERS; runs: $PROFILE_RUNS" + echo "Repetition: $PROFILE_REPETITION; observers: $DWF_GROWTH_OBSERVERS" + echo 'One fresh container per mode, 1 CPU / 1 GiB aggregate memory / no swap.' + echo 'Original 3,600-second workflow and observation bounds remain unchanged.' + echo 'Scalar outcome, CPU, RSS, SQL, cache, queue and timer measurements are in the log.' + echo 'A failed or unfinished original is not acceptance. No database/history archive is uploaded.' + } >> "$GITHUB_STEP_SUMMARY" + if test -d vendor; then rm -r -- vendor; fi + if test -f composer.lock; then rm -- composer.lock; fi diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index f461daf76..742ba3dc2 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -349,6 +349,32 @@ jobs: tests/Unit/V2/ProjectionSqliteContentionTest.php \ --fail-on-warning --log-junit build/test-results/pr-database-queue-recovery.xml + - name: Run retained execution authority and process-loss controls + run: >- + vendor/bin/phpunit tests/Unit/V2/EmbeddedContinuationTest.php + tests/Unit/V2/WorkflowContinuationCacheTest.php + tests/Unit/V2/EmbeddedContinuationProcessTest.php + --testdox --fail-on-warning + --log-junit build/test-results/pr-retained-execution.xml + + - name: Run growing continuation worker setup controls + env: + PROFILE_EMBEDDED_CONTINUATION: setup + run: >- + vendor/bin/phpunit tests/Performance/EmbeddedContinuationGrowthProfileTest.php + --testdox --fail-on-warning --display-phpunit-deprecations + --log-junit build/test-results/pr-continuation-growth.xml + + - name: Run minimally observed continuation setup controls + env: + PROFILE_EMBEDDED_CONTINUATION: setup + DWF_GROWTH_OBSERVERS: 'off' + run: >- + vendor/bin/phpunit tests/Performance/EmbeddedContinuationGrowthProfileTest.php + --filter '10-events_(1-workers_1-runs|4-workers_8-runs)_(replay|retained)_repeat-1' + --testdox --fail-on-warning --fail-on-empty-test-suite + --log-junit build/test-results/pr-continuation-minimal-observers.xml + - name: Resolve bounded pull-request test selection run: >- php scripts/ci/select-pr-tests.php diff --git a/README.md b/README.md index ab040b252..7ce59aae0 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Server or Cloud does not reinterpret existing embedded history. - [Embedded installation](https://durable-workflow.com/docs/2.0/installation/) - [Embedded feature guides](https://durable-workflow.com/docs/2.0/category/embedded/) +- [Embedded live execution](docs/workflow/embedded-live-execution.md) - [Configuration reference](https://durable-workflow.com/docs/2.0/configuration/options/) - [Deployment modes](https://durable-workflow.com/docs/2.0/polyglot/deployment-modes/) - [Monitoring with Waterline](https://durable-workflow.com/docs/2.0/monitoring/) diff --git a/composer.json b/composer.json index e4df20d42..705541e69 100644 --- a/composer.json +++ b/composer.json @@ -80,7 +80,7 @@ "dev-main": "2.0.x-dev" }, "durable-workflow": { - "product-train": "2.7.3", + "product-train": "2.8.0", "laravel-embedded-upgrade-contract": "resources/laravel-embedded-upgrade-contract.json", "laravel-dependency-security-policy": "resources/laravel-dependency-security-policy.json" }, diff --git a/docs/workflow/embedded-live-execution.md b/docs/workflow/embedded-live-execution.md new file mode 100644 index 000000000..d3c94ec77 --- /dev/null +++ b/docs/workflow/embedded-live-execution.md @@ -0,0 +1,134 @@ +# Embedded live execution + +Persistent embedded PHP workers can keep a bounded suspended workflow between +database-queue jobs. An eligible continuation skips its completed instruction +prefix while accepting the same committed durable outcomes. The database +remains authoritative. Ordinary cold replay is the default. + +The [qualification record](https://github.com/durable-workflow/workflow/issues/748) +describes the measured workloads, failed capacity cells and operating limits. +These observations do not establish general production performance or capacity. +Service-mode warm execution has a separate [PHP SDK implementation](https://github.com/durable-workflow/sdk-php/issues/113). + +## Enable a selected workflow + +In the application's published `config/workflows.php`, set an explicit allowlist +and a build identity that changes whenever the workflow deployment changes: + +```php +'v2' => [ + // Other application settings remain here. + 'live_execution' => [ + 'workflows' => [App\Workflows\OrderWorkflow::class], + 'build' => env('APP_RELEASE_ID'), + ], +], +``` + +Set the application's `APP_RELEASE_ID` to its immutable release identifier. +Restart workers when deploying code or changing connections, schemas, bindings +or allowlists. An empty allowlist or missing/empty build identity selects cold +replay. Each process owns its cache; another worker can always cold replay. +There is no wait for a previous worker or extension of a task lease. + +Use an asynchronous Laravel database queue and a persistent `queue:work` +process. Match its connection/queue to the application's existing workflow +routing. Octane is optional and is not part of this cache's lifetime. +Run workers under a process supervisor that restarts memory-limit exits. +An ordinary memory stop can occur between committed jobs; a fresh worker +continues from durable state with an empty cache. Retention does not keep +workers alive or replace supervision. + +## Initial execution profile + +The retained profile supports sequential remote activities and positive ordinary +timers. Activities continue to run through their durable jobs. Timer deadlines +remain persisted and use the workflow's recorded time. + +Allowlist workflows only when their suspended state contains deterministic +workflow data. Keep external I/O and container services in activities. Do not +retain request/scoped services, logging contexts or transient custom-model state +in workflow locals across durable calls. Ordinary replay remains available for +workflows that need those lifetimes. + +The runtime automatically selects cold replay for constructor or method object +injection, explicit workflow bindings, decorated methods/classes, unsupported +history, fake/synchronous jobs and caller-owned transactions. Query/update/signal +handlers, parallel/selection/child operations, conditions, continue-as-new, +version markers, side effects and other excluded operations retain their cold +contract. An excluded operation encountered after a retained prefix triggers +cold replay before its durable handler. Repair/redrive history can also select +cold replay. Recorded successful business effects are not repeated. +The initial retained profile requires declared scalar or array entry-parameter +types. Untyped, union/intersection, object, mixed, callable and iterable +parameters select cold replay, including when their current argument is null. + +Tracing integration is still an explicit qualification gate: a retained Fiber +must never carry an ended processing span or a previous job's task context. +The tracing-enabled cold fallback and context-toggle controls must pass before +the integrated feature is released. + +## Bounds and recovery + +Each worker keeps at most four entries. An entry's 30-second monotonic age is +not renewed by hits. Publication and lookup check a 128 MiB worker-resident-memory +threshold. This includes the worker's other memory and is not an isolated frame +measurement or an OS memory limit. Missing resident-memory observations select +cold replay. Use the normal worker/process supervisor limits as well. + +Before a resume, the worker verifies the backend/session, namespace/run/model, +configured model classes, definition/build, fresh lease, original run fields and complete contiguous +committed history prefix. Reconnects, changed identities, eviction, expiry, +memory pressure and worker loss select authoritative replay. + +An advanced frame becomes reusable only after root commit. Rollback, failed +execution or uncertain acknowledgement discards it. A worker job exception also +clears retained state, including a broker acknowledgement failure after commit. +The ordinary repair and +watchdog paths recover durable work with the original deadlines. Failed task +records remain available for diagnosis. A lost timer transport delivery still +uses the existing stale-dispatch interval; retention does not shorten it. + +Shutdown and eviction discard suspended execution using the existing Fiber +disposal contract. They do not request workflow cancellation or create a new +durable cleanup outcome. External cancellation and termination remain durable +commands and are checked before resuming any frame. + +## Worker diagnostics + +The worker exposes a process-local diagnostic snapshot: + +```php +$snapshot = app(Workflow\V2\Support\WorkflowContinuationCache::class)->snapshot(); +``` + +It reports current entry count, nullable worker resident bytes and cumulative +counters for hits, misses, retention publications, eviction, invalidation and cold-fallback +reasons. `cold_entry` counts fresh authoring starts within the observed profile. +Eviction counters count removed entries; `evict_clear` covers explicit clears +and normal worker shutdown. A repeated pressure check on an empty cache is not +another eviction. +`evict_job_exception` counts entries cleared after a worker job exception. +`reused_committed_operations` counts completed durable operations before the +retained suspension point, and `validated_history_events` counts the fresh +history prefixes checked before reuse. These counts are not PHP instruction +counts or CPU-saving estimates. The diagnostic class is internal. +Collect it through the application's existing worker metrics mechanism. +A web request or separate Artisan process cannot observe another worker's +cache. Counters are sparse and reset when that process exits; resident bytes +cover the whole worker and must not be labelled retained-frame bytes. + +The [reproducible growth probe](../../tests/Performance/EMBEDDED-CONTINUATION.md) +separates cold test-worker startup, full replay and retained execution. Its +reports state actual cache hits/misses, queue/timer delay, CPU, SQL, memory, +sample counts, failures and unfinished work. Choose retention from measurements +of the actual application workload and its supported lifetime constraints. +The [1,000-event comparison](../../tests/Performance/results/issue-748-20261020/GROWTH-1000.md) +passes three single-worker pairs but fails all six four-worker originals in its +one-CPU / 1 GiB shared-SQLite configuration, with unfinished work, SQLite lock +errors and container or worker memory exits in both modes. Its eight-run large +pilots also miss their original observation deadlines in both modes; remaining +large concurrent repetitions were not admitted. Retention does not establish a +larger safe operating envelope. Those failures remain under review. +The [later small and concurrent comparisons](../../tests/Performance/results/issue-748-20261020/CURRENT-SOURCE.md) +report actual cache misses and workload-specific CPU results. diff --git a/scripts/profile-embedded-continuation.sh b/scripts/profile-embedded-continuation.sh new file mode 100755 index 000000000..00b191fca --- /dev/null +++ b/scripts/profile-embedded-continuation.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +events=${1:?Expected 10, 100 or 1000 events} +workers=${2:?Expected 1 or 4 workers} +runs=${3:?Expected 1 or 8 concurrent runs} +repetition=${4:?Expected repetition 1, 2 or 3} +case "$events" in 10|100|1000) ;; *) exit 2 ;; esac +case "$workers" in 1|4) ;; *) exit 2 ;; esac +case "$runs" in 1|8) ;; *) exit 2 ;; esac +case "$repetition" in 1|2|3) ;; *) exit 2 ;; esac +: "${DWF_PROFILE_IMAGE:?Build tests/Performance/continuation first}" + +cd "$(dirname "$0")/.." +git diff --exit-code -- src resources tests scripts composer.json +cmp tests/Performance/continuation/COMPOSER-INPUT.lock composer.lock +git rev-parse HEAD HEAD^{tree} +sha256sum composer.lock +docker image inspect "$DWF_PROFILE_IMAGE" --format '{{.Id}}' + +container='' +cleanup() { + if [[ -n "$container" ]]; then docker container rm --force "$container" >/dev/null; fi +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM +outcome=0 +modes='replay retained' +if [[ "$repetition" == 2 ]]; then modes='retained replay'; fi +for mode in $modes; do + container="dw-continuation-${GITHUB_RUN_ID:-local}-${repetition}-${mode}-$$" + filter="${events}-events_${workers}-workers_${runs}-runs_${mode}_repeat-${repetition}" + echo "CONTINUATION_CELL=$filter" + if timeout --signal=TERM --kill-after=30s 62m \ + docker run --name "$container" --network none \ + --user "$(id -u):$(id -g)" --cpus 1 --memory 1g --memory-swap 1g \ + --volume "$PWD:/app" --workdir /app \ + --env PROFILE_EMBEDDED_CONTINUATION=measure \ + --env "DWF_GROWTH_OBSERVERS=${DWF_GROWTH_OBSERVERS:-on}" \ + "$DWF_PROFILE_IMAGE" \ + php vendor/bin/phpunit tests/Performance/EmbeddedContinuationGrowthProfileTest.php \ + --filter "$filter" --no-coverage --do-not-cache-result \ + --fail-on-empty-test-suite --fail-on-skipped --fail-on-warning; then + status=0 + else + status=$? + outcome=1 + fi + echo "CONTINUATION_CELL_EXIT=$status" + docker container inspect "$container" --format \ + 'CONTINUATION_CONTAINER={{json .Name}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}} cpu={{.HostConfig.NanoCpus}} memory={{.HostConfig.Memory}} memory_swap={{.HostConfig.MemorySwap}}' + cleanup + container='' +done +exit "$outcome" diff --git a/src/Providers/WorkflowServiceProvider.php b/src/Providers/WorkflowServiceProvider.php index 358d50be8..35a9251ed 100644 --- a/src/Providers/WorkflowServiceProvider.php +++ b/src/Providers/WorkflowServiceProvider.php @@ -109,6 +109,7 @@ static function ($app): HistoryProjectionMaintenanceRole { } $this->app->singleton(WorkflowTaskBridge::class, DefaultWorkflowTaskBridge::class); + $this->registerContinuationCache(); $this->app->alias(WorkflowTaskBridge::class, CooperativeWorkflowTaskBridge::class); $this->app->alias(WorkflowTaskBridge::class, PreparedLocalActivityTaskBridge::class); $this->app->alias(WorkflowTaskBridge::class, PreparedLocalActivityGroupTaskBridge::class); @@ -216,6 +217,26 @@ public function boot(): void }); } + private function registerContinuationCache(): void + { + $this->app->singleton(\Workflow\V2\Support\WorkflowContinuationCache::class); + $this->app['events']->listen(\Illuminate\Queue\Events\Looping::class, function (): void { + if ($this->app->resolved(\Workflow\V2\Support\WorkflowContinuationCache::class)) { + $this->app->make(\Workflow\V2\Support\WorkflowContinuationCache::class)->snapshot(); + } + }); + foreach ([ + \Illuminate\Queue\Events\WorkerStopping::class => 'evict_clear', + \Illuminate\Queue\Events\JobExceptionOccurred::class => 'evict_job_exception', + ] as $event => $reason) { + $this->app['events']->listen($event, function () use ($reason): void { + if ($this->app->resolved(\Workflow\V2\Support\WorkflowContinuationCache::class)) { + $this->app->make(\Workflow\V2\Support\WorkflowContinuationCache::class)->clear($reason); + } + }); + } + } + /** * Register observers for long-poll wake signals. */ diff --git a/src/V2/Jobs/RunWorkflowTask.php b/src/V2/Jobs/RunWorkflowTask.php index afd68239a..37217ab3a 100644 --- a/src/V2/Jobs/RunWorkflowTask.php +++ b/src/V2/Jobs/RunWorkflowTask.php @@ -23,6 +23,7 @@ use Workflow\V2\Support\TaskCompatibility; use Workflow\V2\Support\TaskDispatcher; use Workflow\V2\Support\WorkerCompatibilityFleet; +use Workflow\V2\Support\WorkflowContinuationCache; use Workflow\V2\Support\WorkflowExecutor; use Workflow\V2\Support\WorkflowTaskLease; @@ -48,7 +49,7 @@ public function handle(WorkflowExecutor $executor): void is_string($this->queue ?? null) ? $this->queue : null, ); - [$claimed, $deferredTask] = $this->claimTask(); + [$claimed, $deferredTask, $claimedAttempt] = $this->claimTask(); if ($deferredTask instanceof WorkflowTask) { TaskDispatcher::dispatch($deferredTask); @@ -61,9 +62,14 @@ public function handle(WorkflowExecutor $executor): void } $nextTask = null; + $continuation = config('workflows.v2.live_execution.workflows', []) === [] + ? null : app(WorkflowContinuationCache::class)->session($this->job, $claimedAttempt); try { - $nextTask = SqliteTransaction::forTask($this->taskId, function () use ($executor): ?WorkflowTask { + $nextTask = SqliteTransaction::forTask($this->taskId, function () use ( + $executor, + $continuation + ): ?WorkflowTask { /** @var WorkflowTask $task */ $task = ConfiguredV2Models::query('task_model', WorkflowTask::class) ->lockForUpdate() @@ -89,9 +95,18 @@ public function handle(WorkflowExecutor $executor): void return null; } - return $executor->run($run, $task); + if ($continuation !== null && ! $continuation->hasAuthority($run, $task)) { + $continuation->disable('lost_task_authority'); + return null; + } + + $nextTask = $executor->run($run, $task, $continuation); + $continuation?->offer($task); + + return $nextTask; }); } catch (Throwable $throwable) { + $continuation?->disable('transaction_failure'); SqliteTransaction::forTask($this->taskId, function () use ($throwable): void { /** @var WorkflowTask|null $task */ $task = ConfiguredV2Models::query('task_model', WorkflowTask::class) @@ -116,13 +131,15 @@ public function handle(WorkflowExecutor $executor): void throw $throwable; } + $continuation?->committed(); + if ($nextTask instanceof WorkflowTask) { TaskDispatcher::dispatch($nextTask); } } /** - * @return array{0: bool, 1: WorkflowTask|null} + * @return array{0: bool, 1: WorkflowTask|null, 2: int} */ private function claimTask(): array { @@ -133,11 +150,11 @@ private function claimTask(): array ->find($this->taskId); if ($task === null || $task->task_type !== TaskType::Workflow || $task->status !== TaskStatus::Ready) { - return [false, null]; + return [false, null, 0]; } if ($task->available_at !== null && $task->available_at->isFuture()) { - return [false, $task]; + return [false, $task, 0]; } /** @var WorkflowRun $run */ @@ -148,21 +165,23 @@ private function claimTask(): array if (TaskBackendCapabilities::recordClaimFailureIfUnsupported($task) !== null) { $this->projectRun($run->fresh(['instance', 'tasks', 'activityExecutions', 'failures'])); - return [false, null]; + return [false, null, 0]; } if (! TaskCompatibility::supported($task, $run)) { $this->projectRun($run->fresh(['instance', 'tasks', 'activityExecutions', 'failures'])); - return [false, null]; + return [false, null, 0]; } + // The logical task ID is reused as owner; the attempt fences this claim. + $claimedAttempt = (int) $task->attempt_count + 1; $task->forceFill([ 'status' => TaskStatus::Leased, 'leased_at' => now(), 'lease_owner' => $this->taskId, 'lease_expires_at' => WorkflowTaskLease::expiresAt(), - 'attempt_count' => $task->attempt_count + 1, + 'attempt_count' => $claimedAttempt, 'sticky_replay_mode' => StickyExecution::claimReplayMode($task, $this->taskId), 'sticky_claimed_at' => now(), 'last_claim_failed_at' => null, @@ -172,7 +191,7 @@ private function claimTask(): array ServiceCallWorkflowDelivery::recordForClaim($task, $run); $this->projectRun($run->fresh(['instance', 'tasks', 'activityExecutions', 'failures'])); - return [true, null]; + return [true, null, $claimedAttempt]; }); } diff --git a/src/V2/Support/WorkflowContinuation.php b/src/V2/Support/WorkflowContinuation.php new file mode 100644 index 000000000..6884630b6 --- /dev/null +++ b/src/V2/Support/WorkflowContinuation.php @@ -0,0 +1,30 @@ + + */ + public array $prefix = []; + + public string $guard = ''; + + public string $scope = ''; + + public int $retainedAt = 0; + + public function __construct( + public readonly Workflow $workflow, + public readonly WorkflowExecution $execution, + public mixed $current, + public int $sequence = 1, + ) { + } +} diff --git a/src/V2/Support/WorkflowContinuationCache.php b/src/V2/Support/WorkflowContinuationCache.php new file mode 100644 index 000000000..43cab75be --- /dev/null +++ b/src/V2/Support/WorkflowContinuationCache.php @@ -0,0 +1,196 @@ + + */ + private array $entries = []; + + /** + * @var array + */ + private array $generations = []; + + /** + * @var array + */ + private array $counts = []; + + private int $generation = 0; + + /** + * @var WeakMap + */ + private WeakMap $backends; + + private int $backendGeneration = 0; + + public function __construct() + { + $this->backends = new WeakMap(); + } + + public function session(?object $job, int $claimedAttempt): ?WorkflowContinuationSession + { + if (! $job instanceof DatabaseJob || PHP_SAPI !== 'cli' || WorkflowStub::faked() + || config('workflows.v2.live_execution.workflows', []) === []) { + return null; + } + $name = config('workflows.storage.connection'); + $database = DB::connection(is_string($name) ? $name : null); + if ($database->transactionLevel() !== 0 || $database->getPdo()->inTransaction()) { + return null; + } + + return new WorkflowContinuationSession($this, $database, $claimedAttempt); + } + + public function backendIdentity(Connection $database): int + { + // A reconnect must miss even when connection names and persisted rows match. + // Weak keys do not keep disconnected sessions alive or reuse object IDs. + $pdo = $database->getPdo(); + + return $this->backends[$pdo] ??= ++$this->backendGeneration; + } + + public function observe(string $key): int + { + unset($this->generations[$key]); + $this->generations[$key] = ++$this->generation; + // A forgotten generation cannot publish later, even after reentrant commit callbacks. + while (count($this->generations) > 128) { + unset($this->generations[array_key_first($this->generations)]); + } + + return $this->generation; + } + + public function take(string $key, ?string $scope = null): ?WorkflowContinuation + { + $this->trim(); + foreach ($this->entries as $previousKey => $previous) { + if ($scope !== null && $previous->scope === $scope && $previousKey !== $key) { + unset($this->entries[$previousKey]); + $this->count('invalidate_identity'); + } + } + $entry = $this->entries[$key] ?? null; + unset($this->entries[$key]); + $this->count($entry === null ? 'miss' : 'candidate'); + + return $entry; + } + + public function publish(string $key, int $generation, WorkflowContinuation $entry): void + { + $this->trim(); + $now = hrtime(true); + if ($entry->retainedAt !== 0 && $now - $entry->retainedAt >= 30_000_000_000) { + $this->count('discard_age'); + return; + } + $scope = $entry->scope === '' ? $key : $entry->scope; + if (($this->generations[$scope] ?? null) !== $generation || ! $this->memoryAvailable()) { + $this->count('discard_publication'); + return; + } + unset($this->entries[$key]); + while (count($this->entries) >= 4) { + unset($this->entries[array_key_first($this->entries)]); + $this->count('evict_capacity'); + } + if ($entry->retainedAt === 0) { + $entry->retainedAt = $now; + } + $entry->workflow->run->unsetRelations(); + $this->entries[$key] = $entry; + $this->count('retained'); + } + + public function count(string $reason, int $amount = 1): void + { + $this->counts[$reason] = ($this->counts[$reason] ?? 0) + $amount; + } + + public function invalidate(string $scope): void + { + foreach ($this->entries as $key => $entry) { + if ($entry->scope === $scope) { + unset($this->entries[$key]); + $this->count('invalidate_run'); + } + } + } + + /** + * @return array{entries: int, resident_bytes: ?int, counts: array} + */ + public function snapshot(): array + { + $this->trim(); + + return [ + 'entries' => count($this->entries), + 'resident_bytes' => $this->residentBytes(), + 'counts' => $this->counts, + ]; + } + + public function clear(string $reason = 'evict_clear'): void + { + if ($this->entries !== []) { + $this->count($reason, count($this->entries)); + } + $this->entries = []; + $this->generations = []; + } + + private function trim(): void + { + if (! $this->memoryAvailable()) { + if ($this->entries !== []) { + $this->count('evict_memory', count($this->entries)); + } + $this->entries = []; + return; + } + $now = hrtime(true); + foreach ($this->entries as $key => $entry) { + if ($now - $entry->retainedAt >= 30_000_000_000) { + unset($this->entries[$key]); + $this->count('evict_age'); + } + } + } + + private function memoryAvailable(): bool + { + $bytes = $this->residentBytes(); + + return $bytes !== null && $bytes < 128 * 1024 * 1024; + } + + private function residentBytes(): ?int + { + $status = is_readable('/proc/self/status') ? file_get_contents('/proc/self/status') : false; + if (! is_string($status) || preg_match('/^VmRSS:\s+(\d+) kB$/m', $status, $match) !== 1) { + return null; + } + + return (int) $match[1] * 1024; + } +} diff --git a/src/V2/Support/WorkflowContinuationSession.php b/src/V2/Support/WorkflowContinuationSession.php new file mode 100644 index 000000000..9ad3afd48 --- /dev/null +++ b/src/V2/Support/WorkflowContinuationSession.php @@ -0,0 +1,260 @@ + $class + */ + public function begin(WorkflowRun $run, WorkflowTask $task, string $class): ?WorkflowContinuation + { + $this->observe($run); + $build = config('workflows.v2.live_execution.build'); + $eligible = is_string($build) && $build !== '' && $this->eligibleClass($class) + && $this->database === $run->getConnection() && $this->database->transactionLevel() === 1 + && $this->hasAuthority($run, $task) && $this->safeHistory($run); + $prefix = $eligible ? $this->prefix($run) : null; + if ($prefix === null) { + $this->disable($eligible ? 'cold_history_cursor' : 'cold_profile'); + return null; + } + $this->cache->count('validated_history_events', count($prefix)); + $this->key = hash('sha256', serialize([ + $this->scope, $this->cache->backendIdentity($this->database), + $run::class, $this->configuredModels(), $class, WorkflowDefinition::fingerprint($class), + $build, config('workflows.v2.compatibility.current'), PHP_VERSION, + ])); + $entry = $this->cache->take($this->key, $this->scope); + if ($entry !== null && ($entry->guard !== $this->guard($run) + || $entry->prefix !== array_slice($prefix, 0, count($entry->prefix)))) { + $this->cache->count('invalidate_history_or_run'); + return null; + } + if ($entry !== null) { + // Keep references captured by authoring code attached to the refreshed model. + $bound = $entry->workflow->run; + $bound->setRawAttributes($run->getAttributes(), true); + $bound->setRelations($run->getRelations()); + $this->resumed = true; + $this->cache->count('hit'); + $this->cache->count('reused_committed_operations', max(0, $entry->sequence - 1)); + } + + return $entry; + } + + public function hasAuthority(WorkflowRun $run, WorkflowTask $task): bool + { + $this->observe($run); + return $task->workflow_run_id === $run->id && $task->namespace === $run->namespace + && $task->status === TaskStatus::Leased && $task->lease_owner === $task->id + && $this->claimedAttempt > 0 && (int) $task->attempt_count === $this->claimedAttempt + && $task->lease_expires_at !== null + && $task->lease_expires_at->isFuture(); + } + + public function accepts(mixed $call): bool + { + return $this->enabled && ($call instanceof ActivityCall || ($call instanceof TimerCall && $call->seconds > 0)); + } + + public function resumed(): bool + { + return $this->resumed; + } + + public function recordColdEntry(): void + { + $this->cache->count('cold_entry'); + } + + public function disable(string $reason = 'discard'): void + { + $this->cache->count($reason); + if ($this->scope !== null) { + $this->cache->invalidate($this->scope); + } + $this->enabled = false; + $this->pending = null; + $this->active = null; + } + + public function track(WorkflowContinuation $entry): void + { + $this->active = $this->enabled ? $entry : null; + } + + public function offer(WorkflowTask $task): void + { + $entry = $this->active; + $this->active = null; + if ($entry === null) { + return; + } + $run = $entry->workflow->run; + if (! $this->accepts($entry->current) || ! $entry->execution->valid() + || $run->status !== RunStatus::Waiting || $task->status !== TaskStatus::Completed + || ! $this->safeHistory($run)) { + return; + } + $entry->guard = $this->guard($run); + $prefix = $this->prefix($run); + if ($prefix !== null) { + $entry->scope = $this->scope ?? ''; + $entry->prefix = $prefix; + $this->pending = $entry; + } + } + + public function committed(): void + { + $pending = $this->pending; + $this->pending = null; + if ($pending === null || $this->key === null || $this->database->transactionLevel() !== 0 + || $this->database->getPdo() + ->inTransaction()) { + return; + } + $this->cache->publish($this->key, $this->generation, $pending); + } + + /** + * @return array + */ + private function configuredModels(): array + { + $models = []; + $configuration = config('workflows.v2', []); + foreach (is_array($configuration) ? $configuration : [] as $key => $value) { + if (is_string($key) && str_ends_with($key, '_model')) { + // Invalid non-string settings already resolve to the ordinary model. + $models[$key] = is_string($value) ? $value : get_debug_type($value); + } + } + ksort($models); + + return $models; + } + + /** + * @param class-string $class + */ + private function eligibleClass(string $class): bool + { + $allowed = config('workflows.v2.live_execution.workflows', []); + if (! is_array($allowed) || ! in_array($class, $allowed, true) || app()->bound($class)) { + return false; + } + $reflection = new ReflectionClass($class); + $unsupportedAttributes = array_filter( + $reflection->getAttributes(), + static fn ($attribute): bool => $attribute->getName() !== Type::class + ); + $decoratedMethods = array_filter( + $reflection->getMethods(), + static fn ($method): bool => $method->getAttributes() !== [] + ); + $unsupportedParameters = array_filter( + EntryMethod::forWorkflow($class)->getParameters(), + static fn ($parameter): bool => ! $parameter->getType() instanceof ReflectionNamedType + || ! in_array($parameter->getType()->getName(), [ + 'int', 'float', 'string', 'bool', 'array', 'null', 'true', 'false', + ], true) + ); + + return $reflection->getConstructor() === null && $unsupportedAttributes === [] + && $decoratedMethods === [] && $unsupportedParameters === []; + } + + private function safeHistory(WorkflowRun $run): bool + { + foreach ($run->historyEvents as $event) { + if (! in_array($event->event_type, [HistoryEventType::StartAccepted, HistoryEventType::WorkflowStarted, + HistoryEventType::ActivityScheduled, HistoryEventType::ActivityStarted, + HistoryEventType::ActivityHeartbeatRecorded, HistoryEventType::ActivityCompleted, + HistoryEventType::TimerScheduled, HistoryEventType::TimerFired], true)) { + return false; + } + } + + return true; + } + + /** + * @return list|null + */ + private function prefix(WorkflowRun $run): ?array + { + $events = $run->historyEvents() + ->get(); + if ((int) $run->last_history_sequence !== $events->count()) { + return null; + } + foreach ($events->values() as $index => $event) { + if ((int) $event->sequence !== $index + 1) { + return null; + } + } + + return $events->map(static fn ($event): string => hash('sha256', serialize($event->getRawOriginal()))) + ->all(); + } + + private function observe(WorkflowRun $run): void + { + if ($this->scope === null) { + $this->scope = hash('sha256', serialize([ + spl_object_id(app()), $this->database->getName(), $this->database->getDriverName(), + $this->database->getDatabaseName(), $this->database->getTablePrefix(), + $this->database->getConfig('search_path'), $run->getTable(), $run->namespace, + $run->workflow_instance_id, $run->id, + ])); + $this->generation = $this->cache->observe($this->scope); + } + } + + private function guard(WorkflowRun $run): string + { + return hash('sha256', serialize($run->only([ + 'namespace', 'workflow_instance_id', 'id', 'workflow_class', 'workflow_type', 'arguments', + 'payload_codec', 'started_at', 'execution_deadline_at', 'run_deadline_at', + 'connection', 'queue', 'compatibility', + ]))); + } +} diff --git a/src/V2/Support/WorkflowExecution.php b/src/V2/Support/WorkflowExecution.php index 5da4c9ea9..952bcd1db 100644 --- a/src/V2/Support/WorkflowExecution.php +++ b/src/V2/Support/WorkflowExecution.php @@ -62,7 +62,12 @@ public static function startCallback( WorkflowFiberContext::setTime($eventTime, $fiber); } - $current = $fiber->start(); + $sample = WorkflowExecutionProfile::begin('authoring_start'); + try { + $current = $fiber->start(); + } finally { + WorkflowExecutionProfile::finish($sample); + } if ($fiber->isSuspended()) { return new self(fiber: $fiber, current: $current); @@ -111,7 +116,12 @@ public function send( WorkflowFiberContext::observeCancellationTime(null, $this->fiber); } - $result = $this->fiber->resume($value); + $sample = WorkflowExecutionProfile::begin('authoring_resume'); + try { + $result = $this->fiber->resume($value); + } finally { + WorkflowExecutionProfile::finish($sample); + } if ($this->fiber->isSuspended()) { $this->current = $result; @@ -145,7 +155,12 @@ public function throw( WorkflowFiberContext::observeCancellationTime(null, $this->fiber); } - $result = $this->fiber->throw($throwable); + $sample = WorkflowExecutionProfile::begin('authoring_throw'); + try { + $result = $this->fiber->throw($throwable); + } finally { + WorkflowExecutionProfile::finish($sample); + } if ($this->fiber->isSuspended()) { $this->current = $result; diff --git a/src/V2/Support/WorkflowExecutionProfile.php b/src/V2/Support/WorkflowExecutionProfile.php new file mode 100644 index 000000000..05a97d1cd --- /dev/null +++ b/src/V2/Support/WorkflowExecutionProfile.php @@ -0,0 +1,109 @@ + + */ + private array $stack = []; + + /** + * @var array + */ + private array $phases = []; + + private bool $balanced = true; + + public function collect(callable $operation): mixed + { + $previous = self::$active; + self::$active = $this; + try { + return $operation(); + } finally { + $this->balanced = $this->balanced && $this->stack === []; + self::$active = $previous; + } + } + + public static function begin(string $phase): ?int + { + $profile = self::$active; + if ($profile === null) { + return null; + } + $profile->stack[] = [ + 'phase' => $phase, + 'cpu' => self::cpu(), + 'wall' => hrtime(true), + 'child_cpu' => 0, + 'child_wall' => 0, + ]; + + return count($profile->stack); + } + + public static function finish(?int $depth): void + { + $profile = self::$active; + if ($depth === null || $profile === null) { + return; + } + $cpu = self::cpu(); + $wall = hrtime(true); + if ($depth !== count($profile->stack)) { + $profile->balanced = false; + return; + } + $sample = array_pop($profile->stack); + $cpu -= $sample['cpu']; + $wall -= $sample['wall']; + $phase = &$profile->phases[$sample['phase']]; + $phase ??= [ + 'calls' => 0, + 'cpu_us' => 0, + 'wall_ns' => 0, + 'exclusive_cpu_us' => 0, + 'exclusive_wall_ns' => 0, + ]; + ++$phase['calls']; + $phase['cpu_us'] += $cpu; + $phase['wall_ns'] += $wall; + $phase['exclusive_cpu_us'] += max(0, $cpu - $sample['child_cpu']); + $phase['exclusive_wall_ns'] += max(0, $wall - $sample['child_wall']); + $parent = count($profile->stack) - 1; + if ($parent >= 0) { + $profile->stack[$parent]['child_cpu'] += $cpu; + $profile->stack[$parent]['child_wall'] += $wall; + } + } + + /** + * @return array{balanced: bool, phases: array} + */ + public function snapshot(): array + { + return [ + 'balanced' => $this->balanced && $this->stack === [], + 'phases' => $this->phases, + ]; + } + + private static function cpu(): int + { + $usage = getrusage(); + + return ($usage['ru_utime.tv_sec'] + $usage['ru_stime.tv_sec']) * 1_000_000 + + $usage['ru_utime.tv_usec'] + $usage['ru_stime.tv_usec']; + } +} diff --git a/src/V2/Support/WorkflowExecutor.php b/src/V2/Support/WorkflowExecutor.php index d8264f059..32c98ae78 100644 --- a/src/V2/Support/WorkflowExecutor.php +++ b/src/V2/Support/WorkflowExecutor.php @@ -60,2426 +60,2417 @@ final class WorkflowExecutor { - public function run(WorkflowRun $run, WorkflowTask $task): ?WorkflowTask + public function run( + WorkflowRun $run, + WorkflowTask $task, + ?WorkflowContinuationSession $continuation = null, + ): ?WorkflowTask { + $sample = WorkflowExecutionProfile::begin('executor'); + try { + return $this->runTask($run, $task, $continuation); + } finally { + WorkflowExecutionProfile::finish($sample); + } + } + + /** + * Close a run whose server-enforced execution or run deadline elapsed. + * + * External worker bridges call this while holding the task and run locks, + * immediately before accepting commands. This makes the deadline the + * authority at the workflow-task commit boundary, even when the watchdog + * cannot repair a run because its task remains leased. + */ + public function timeoutIfDeadlineExpired(WorkflowRun $run, WorkflowTask $task): bool + { + $sample = WorkflowExecutionProfile::begin('initial_history_load'); + try { + $run->load([ + 'instance', + 'activityExecutions', + 'timers', + 'failures', + 'tasks', + 'commands', + 'updates', + 'historyEvents', + 'childLinks.childRun.instance.currentRun', + 'childLinks.childRun.failures', + 'childLinks.childRun.historyEvents', + ]); + } finally { + WorkflowExecutionProfile::finish($sample); + } + + if (! $this->deadlineExpired($run)) { + return false; + } + + $this->timeoutRun($run, $task); + + return true; + } + + /** + * Finish portable cleanup with the original cancellation outcome. + * The caller holds the task and run locks before accepting a terminal command. + */ + public function finishPortableCooperativeCancellation(WorkflowRun $run, WorkflowTask $task): bool + { + if (! is_string($run->cancellation_request_command_id)) { + return false; + } + $run->load([ + 'instance', 'activityExecutions', 'timers', 'failures', 'tasks', 'commands', + 'updates', 'historyEvents', 'childLinks.childRun.instance.currentRun', + 'childLinks.childRun.failures', 'childLinks.childRun.historyEvents', + ]); + $this->finishCooperativeCancellation($run, $task, 'Cooperative cancellation completed.'); + + return true; + } + + public function cancelIfCleanupDeadlineExpired(WorkflowRun $run, WorkflowTask $task): bool + { + if ( + $run->status->isTerminal() + || ! is_string($run->cancellation_request_command_id) + || $run->cancellation_deadline_at === null + || now() + ->lt($run->cancellation_deadline_at) + ) { + return false; + } + + $run->load([ + 'instance', + 'activityExecutions', + 'timers', + 'failures', + 'tasks', + 'commands', + 'updates', + 'historyEvents', + 'childLinks.childRun.instance.currentRun', + 'childLinks.childRun.failures', + 'childLinks.childRun.historyEvents', + ]); + $this->finishCooperativeCancellation($run, $task, 'Cooperative cancellation cleanup deadline expired.'); + + return true; + } + + /** + * The caller holds the parent run lock; child terminal history, not row status, authorizes recovery. + */ + public function recoverClosedChildResolutions(WorkflowRun $parentRun): void { - if ($this->timeoutIfDeadlineExpired($run, $task)) { + if ($parentRun->status->isTerminal() || $parentRun->tasks() + ->where('task_type', TaskType::Workflow->value) + ->whereIn('status', [TaskStatus::Ready->value, TaskStatus::Leased->value]) + ->lockForUpdate() + ->exists()) { + return; + } + + $parentRun->setRelation('historyEvents', $parentRun->historyEvents()->lockForUpdate()->get()); + foreach (ChildRunHistory::knownSequences($parentRun) as $sequence) { + if (ChildRunHistory::resolutionEventForSequence($parentRun, $sequence) !== null) { + continue; + } + try { + WorkflowStepHistory::assertCompatible($parentRun, $sequence, WorkflowStepHistory::CHILD_WORKFLOW); + WorkflowStepHistory::assertTypedHistoryRecorded( + $parentRun, + $sequence, + WorkflowStepHistory::CHILD_WORKFLOW + ); + } catch (HistoryEventShapeMismatchException) { + continue; + } + + $childRun = ChildRunHistory::childRunForSequence($parentRun, $sequence); + if (! $childRun instanceof WorkflowRun || ! $childRun->historyEvents->contains( + static fn (WorkflowHistoryEvent $event): bool => in_array($event->event_type, [ + HistoryEventType::WorkflowCompleted, + HistoryEventType::WorkflowFailed, + HistoryEventType::WorkflowCancelled, + HistoryEventType::WorkflowTerminated, + ], true) + )) { + continue; + } + if ($this->startChildRetryIfAvailable($parentRun, $sequence, $childRun) === null) { + $this->recordChildResolution($parentRun, null, $sequence, $childRun); + } + $parentRun->unsetRelation('childLinks'); + $parentRun->setRelation('historyEvents', $parentRun->historyEvents()->lockForUpdate()->get()); + } + } + + public function deliverPortableCancellation( + WorkflowRun $run, + WorkflowTask $task, + int $sequence, + string $callKind, + int $sequenceSpan = 1, + ?int $operationSequence = null, + int $operationSequenceSpan = 1, + ): ?WorkflowHistoryEvent { + $existing = CooperativeCancellationDelivery::recorded($run); + if ($existing instanceof WorkflowHistoryEvent) { + return $existing; + } + $run->loadMissing(['tasks', 'timers', 'activityExecutions']); + $targetSequence = $operationSequence ?? $sequence; + $targetSpan = $operationSequence === null ? $sequenceSpan : $operationSequenceSpan; + $waiting = false; + for ($offset = 0; $offset < $targetSpan; ++$offset) { + $targetKind = in_array($callKind, ['parallel', 'selection_handle'], true) + ? CooperativeCancellationDelivery::callKindAt($run, $targetSequence + $offset) + : $callKind; + if ($targetKind !== null) { + $waiting = $this->cancelOpenWaitAtSequence( + $run, + $task, + $targetSequence + $offset, + $targetKind + ) || $waiting; + } + } + if ($waiting) { + ActivityCancellationWait::bindBoundary( + $task, + $sequence, + $callKind, + $sequenceSpan, + $operationSequence, + $operationSequenceSpan + ); + $this->waitForNextResumeSource($run, $task); + return null; } - if ($this->cancelIfCleanupDeadlineExpired($run, $task)) { + return CooperativeCancellationDelivery::record( + $run, + $task, + $sequence, + $callKind, + $sequenceSpan, + $operationSequence, + $operationSequenceSpan, + ); + } + + private function runTask( + WorkflowRun $run, + WorkflowTask $task, + ?WorkflowContinuationSession $continuation, + ): ?WorkflowTask { + if ($this->timeoutIfDeadlineExpired($run, $task) || $this->cancelIfCleanupDeadlineExpired($run, $task)) { return null; } - $workflowClass = WorkflowDefinitionFingerprint::resolveClassForRun($run); - $workflow = RuntimeObjectFactory::workflow($workflowClass, $run); - $entryMethod = EntryMethod::forWorkflow($workflow); + $sample = WorkflowExecutionProfile::begin('initialization'); try { - $arguments = WorkflowEntryArguments::resolve($workflow, $run->workflowArguments(), $entryMethod); - } catch (WorkflowEntryArgumentsMismatchException $exception) { - $this->blockReplayUntilCompatibleEntryArguments($run, $task, $exception); - + $state = $this->prepareExecution($run, $task, $continuation); + } finally { + WorkflowExecutionProfile::finish($sample); + } + if ($state === null) { return null; } + return $this->advanceExecution($state, $continuation); + } + + private function prepareExecution( + WorkflowRun $run, + WorkflowTask $task, + ?WorkflowContinuationSession $continuation, + ): ?WorkflowTaskExecution { + $workflowClass = WorkflowDefinitionFingerprint::resolveClassForRun($run); + $frame = $continuation?->begin($run, $task, $workflowClass); + $workflow = $frame?->workflow ?? RuntimeObjectFactory::workflow($workflowClass, $run); + $run = $workflow->run; + $arguments = []; + if ($frame === null) { + $entryMethod = EntryMethod::forWorkflow($workflow); + try { + $arguments = WorkflowEntryArguments::resolve($workflow, $run->workflowArguments(), $entryMethod); + } catch (WorkflowEntryArgumentsMismatchException $exception) { + $continuation?->disable('entry_arguments_mismatch'); + $this->blockReplayUntilCompatibleEntryArguments($run, $task, $exception); + + return null; + } + } + try { $versionDecisions = new VersionDecisions($run); - $workflowExecution = WorkflowExecution::start($workflow, $arguments, RedriveReplayClock::startedAt($run)); + if ($frame === null) { + $continuation?->recordColdEntry(); + $execution = WorkflowExecution::start($workflow, $arguments, RedriveReplayClock::startedAt($run)); + $frame = new WorkflowContinuation($workflow, $execution, $execution->current()); + } } catch (Throwable $throwable) { $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - return null; } - if (! $workflowExecution->valid()) { + if (! $frame->execution->valid()) { try { - $this->completeRun($run, $task, $workflowExecution->getReturn()); + $this->completeRun($run, $task, $frame->execution->getReturn()); } catch (Throwable $throwable) { $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); } - return null; } - $current = $workflowExecution->current(); + $continuation?->track($frame); + $this->syncWorkflowCursor($workflow, $frame->sequence); - $sequence = 1; - $this->syncWorkflowCursor($workflow, $sequence); - $historySequenceAtTaskStart = $run->last_history_sequence ?? 0; + return new WorkflowTaskExecution( + $run, + $task, + $frame, + $versionDecisions, + $workflowClass, + (int) ($run->last_history_sequence ?? 0), + ); + } + private function advanceExecution( + WorkflowTaskExecution $state, + ?WorkflowContinuationSession $continuation, + ): ?WorkflowTask { while (true) { - try { - while (($duplicate = $versionDecisions->duplicateAt( - $sequence, - $current instanceof VersionCall ? $current : null, - )) !== null) { - $versionDecisions->assertDuplicate($run, $duplicate, $sequence); - WorkflowStepHistory::assertCompatible($run, $sequence, WorkflowStepHistory::VERSION_MARKER, [ - 'change_id' => $duplicate->payload['change_id'], - ]); - ++$sequence; - $this->syncWorkflowCursor($workflow, $sequence); - } - ActivityReplayIdentity::assertCall($run, $current, $sequence); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; + $result = $this->executeStep($state, $continuation); + if ($result !== WorkflowTaskStep::ContinueExecution) { + return $result; } + } + } - $eventsInTransaction = ($run->last_history_sequence ?? 0) - $historySequenceAtTaskStart; - - if ($eventsInTransaction > 0) { - try { - StructuralLimits::guardHistoryTransactionSize($eventsInTransaction); - $this->logApproachingLimit( - StructuralLimits::warnApproachingHistoryTransaction($eventsInTransaction), - $run - ); - } catch (StructuralLimitExceededException $limitExceeded) { - $this->failRun($run, $task, $limitExceeded, 'workflow_run', $run->id); - - return null; - } + private function executeStep( + WorkflowTaskExecution $state, + ?WorkflowContinuationSession &$continuation, + ): WorkflowTask|WorkflowTaskStep|null { + $frame = $state->frame; + if ($continuation !== null && $frame->execution->valid() && ! $continuation->accepts($frame->current)) { + $resumed = $continuation->resumed(); + $continuation->disable('cold_unsupported_operation'); + if ($resumed) { + $continuation->recordColdEntry(); } + $continuation = null; + if ($resumed) { + return $this->run($state->run, $state->task); + } + } - if (! $workflowExecution->valid()) { - try { - $this->syncWorkflowCursor($workflow, $sequence); - $this->completeRun($run, $task, $workflowExecution->getReturn()); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - } - + $sample = WorkflowExecutionProfile::begin('step_validation'); + try { + if (! $this->validateCurrentStep($state) || ! $this->guardHistoryTransaction($state)) { return null; } + } finally { + WorkflowExecutionProfile::finish($sample); + } - if ($current instanceof CancelDurableOperationCall) { - try { - $this->cancelDurableOperation($run, $task, $current->handle); - $this->syncWorkflowCursor($workflow, $sequence); - $current = $workflowExecution->send(null); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } + $sample = WorkflowExecutionProfile::begin('durable_call_dispatch'); + try { + return $this->executeCurrentCall($state); + } finally { + WorkflowExecutionProfile::finish($sample); + } + } - continue; + private function validateCurrentStep(WorkflowTaskExecution $state): bool + { + $run = $state->run; + $frame = $state->frame; + try { + while (($duplicate = $state->versionDecisions->duplicateAt( + $frame->sequence, + $frame->current instanceof VersionCall ? $frame->current : null, + )) !== null) { + $state->versionDecisions->assertDuplicate($run, $duplicate, $frame->sequence); + WorkflowStepHistory::assertCompatible($run, $frame->sequence, WorkflowStepHistory::VERSION_MARKER, [ + 'change_id' => $duplicate->payload['change_id'], + ]); + ++$frame->sequence; + $this->syncWorkflowCursor($frame->workflow, $frame->sequence); } + ActivityReplayIdentity::assertCall($run, $frame->current, $frame->sequence); + } catch (Throwable $throwable) { + $this->failRun($run, $state->task, $throwable, 'workflow_run', $run->id); + return false; + } - if ($current instanceof DurableOperationHandle) { - $resolution = $this->durableOperationResolution($run, $current); - - $handleResolution = $this->selectionOperationCancelledEvent($run, $current); - if (! $handleResolution instanceof WorkflowHistoryEvent) { - $handleResolution = $current->call instanceof AllCall - ? $this->parallelResolutionEvent( - $run, - $current->call, - $current->baseSequence, - $current->call->leafDescriptors($current->baseSequence), - ) - : $this->waitResolutionEvent($run, $current->call, $current->baseSequence); - } + return true; + } - $cancellationDelivery = $this->deliverCancellationAtCall( - $run, - $task, - $sequence, - 'selection_handle', - $handleResolution, - $workflowExecution, - null, - $current->baseSequence, - $current->size, - ); - if ($cancellationDelivery === CancellationDeliveryState::Waiting) { - return $this->waitForNextResumeSource($run, $task); - } - if ($cancellationDelivery === CancellationDeliveryState::Delivered) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->throw( - new WorkflowCancellationRequestedException( - cancellation: CooperativeCancellationDelivery::context($run), - ), - $run->cancellation_delivered_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } + private function guardHistoryTransaction(WorkflowTaskExecution $state): bool + { + $run = $state->run; + $eventsInTransaction = ($run->last_history_sequence ?? 0) - $state->historySequenceAtTaskStart; + if ($eventsInTransaction <= 0) { + return true; + } + try { + StructuralLimits::guardHistoryTransactionSize($eventsInTransaction); + $this->logApproachingLimit(StructuralLimits::warnApproachingHistoryTransaction($eventsInTransaction), $run); + } catch (StructuralLimitExceededException $limitExceeded) { + $this->failRun($run, $state->task, $limitExceeded, 'workflow_run', $run->id); + return false; + } - ++$sequence; - continue; - } + return true; + } - if (! $resolution['resolved']) { - $this->syncWorkflowCursor($workflow, $sequence); + private function executeCurrentCall(WorkflowTaskExecution $state): WorkflowTask|WorkflowTaskStep|null + { + if (! $state->frame->execution->valid()) { + $this->finishExecution($state); + return null; + } + $current = $state->frame->current; + return match (true) { + $current instanceof CancelDurableOperationCall => $this->executeCancelDurableOperation( + $state->run, + $state->task, + $state->frame + ), + $current instanceof DurableOperationHandle => $this->executeDurableOperation( + $state->run, + $state->task, + $state->frame + ), + $current instanceof LocalActivityCall => $this->executeLocalActivity( + $state->run, + $state->task, + $state->frame + ), + $current instanceof ActivityCall => $this->executeActivity($state->run, $state->task, $state->frame), + $current instanceof AwaitCall || $current instanceof AwaitWithTimeoutCall => $this->executeConditionWait( + $state->run, + $state->task, + $state->frame + ), + $current instanceof SideEffectCall => $this->executeSideEffect($state->run, $state->task, $state->frame), + $current instanceof VersionCall => $this->executeVersion( + $state->run, + $state->task, + $state->frame, + $state->versionDecisions + ), + $current instanceof UpsertSearchAttributesCall => $this->executeSearchAttributes( + $state->run, + $state->task, + $state->frame + ), + $current instanceof UpsertMemoCall => $this->executeMemo($state->run, $state->task, $state->frame), + $current instanceof TimerCall => $this->executeTimer($state->run, $state->task, $state->frame), + $current instanceof SignalCall => $this->executeSignal($state->run, $state->task, $state->frame), + $current instanceof ServiceOperationCall => $this->executeServiceOperation( + $state->run, + $state->task, + $state->frame + ), + $current instanceof ChildWorkflowCall => $this->executeChild($state->run, $state->task, $state->frame), + $current instanceof AllCall => $this->executeAll($state->run, $state->task, $state->frame), + $current instanceof ContinueAsNewCall => $this->executeContinueAsNew( + $state->run, + $state->task, + $state->frame, + $state->workflowClass + ), + default => $this->unsupportedCall($state), + }; + } - return $this->waitForNextResumeSource($run, $task, true); - } + private function finishExecution(WorkflowTaskExecution $state): void + { + $run = $state->run; + try { + $this->syncWorkflowCursor($state->frame->workflow, $state->frame->sequence); + $this->completeRun($run, $state->task, $state->frame->execution->getReturn()); + } catch (Throwable $throwable) { + $this->failRun($run, $state->task, $throwable, 'workflow_run', $run->id); + } + } - try { - $this->syncWorkflowCursor($workflow, $sequence); - $current = $resolution['failure'] instanceof Throwable - ? $workflowExecution->throw($resolution['failure'], $resolution['recorded_at']) - : $workflowExecution->send($resolution['value'], $resolution['recorded_at']); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } + private function unsupportedCall(WorkflowTaskExecution $state): ?WorkflowTask + { + $this->failRun( + $state->run, + $state->task, + new UnsupportedWorkflowYieldException(sprintf( + 'Workflow %s yielded %s. v2 currently supports activity(), child(), async(), all(), parallel(), await(), signal(), timer(), sideEffect(), continueAsNew(), getVersion(), patched(), deprecatePatch(), upsertMemo(), and upsertSearchAttributes() only.', + $state->run->workflow_class, + get_debug_type($state->frame->current), + )), + 'workflow_run', + $state->run->id, + ); - continue; - } + return null; + } - if ($current instanceof LocalActivityCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + private function executeCancelDurableOperation( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var CancelDurableOperationCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } + try { + $this->cancelDurableOperation($run, $task, $current->handle); + $this->syncWorkflowCursor($workflow, $sequence); + $current = $workflowExecution->send(null); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + return WorkflowTaskStep::ContinueExecution; + } + + private function executeDurableOperation( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var DurableOperationHandle $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $resolution = $this->durableOperationResolution($run, $current); + $handleResolution = $this->selectionOperationCancelledEvent($run, $current); + if (! $handleResolution instanceof WorkflowHistoryEvent) { + $handleResolution = $current->call instanceof AllCall ? $this->parallelResolutionEvent( + $run, + $current->call, + $current->baseSequence, + $current->call->leafDescriptors($current->baseSequence) + ) : $this->waitResolutionEvent($run, $current->call, $current->baseSequence); + } + $cancellationDelivery = $this->deliverCancellationAtCall( + $run, + $task, + $sequence, + 'selection_handle', + $handleResolution, + $workflowExecution, + null, + $current->baseSequence, + $current->size + ); + if ($cancellationDelivery === CancellationDeliveryState::Waiting) { + return $this->waitForNextResumeSource($run, $task); + } + if ($cancellationDelivery === CancellationDeliveryState::Delivered) { + return $this->resumeCancellation($run, $task, $frame); + } + if (! $resolution['resolved']) { + $this->syncWorkflowCursor($workflow, $sequence); + return $this->waitForNextResumeSource($run, $task, true); + } + try { + $this->syncWorkflowCursor($workflow, $sequence); + $current = $resolution['failure'] instanceof Throwable ? $workflowExecution->throw( + $resolution['failure'], + $resolution['recorded_at'] + ) : $workflowExecution->send($resolution['value'], $resolution['recorded_at']); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + return WorkflowTaskStep::ContinueExecution; + } - if (! $this->ensureStepHistoryCompatible( + private function executeLocalActivity( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var LocalActivityCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::LOCAL_ACTIVITY, [ + 'activity_type' => $current->activity, + ])) { + return null; + } + $localOutcome = (new LocalActivityExecutor())->execute($run, $task, $sequence, $current); + if ($localOutcome['status'] === 'waiting') { + $this->syncWorkflowCursor($workflow, $sequence + 1); + $this->waitForNextResumeSource($run, $task); + return $localOutcome['next_task']; + } + $activityCompletion = $localOutcome['event']; + if (! $activityCompletion instanceof WorkflowHistoryEvent) { + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->waitForNextResumeSource($run, $task); + } + try { + $this->syncWorkflowCursor($workflow, $sequence + 1); + if ($activityCompletion->event_type === HistoryEventType::ActivityCompleted) { + $current = $workflowExecution->send( + $this->activityResult($activityCompletion, $run), + $activityCompletion->recorded_at + ); + } else { + $failureId = $activityCompletion->payload['failure_id'] ?? null; + $current = $workflowExecution->throw( + $this->activityException($activityCompletion, null, $run), + $activityCompletion->recorded_at + ); + $this->recordFailureHandled( $run, $task, + is_string($failureId) ? $failureId : null, $sequence, - WorkflowStepHistory::LOCAL_ACTIVITY, - [ - 'activity_type' => $current->activity, - ], - )) { - return null; - } - - $localOutcome = (new LocalActivityExecutor())->execute($run, $task, $sequence, $current); - - if ($localOutcome['status'] === 'waiting') { - $this->syncWorkflowCursor($workflow, $sequence + 1); - - $this->waitForNextResumeSource($run, $task); - - return $localOutcome['next_task']; - } - - $activityCompletion = $localOutcome['event']; - - if (! $activityCompletion instanceof WorkflowHistoryEvent) { - $this->syncWorkflowCursor($workflow, $sequence + 1); - - return $this->waitForNextResumeSource($run, $task); - } - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - if ($activityCompletion->event_type === HistoryEventType::ActivityCompleted) { - $current = $workflowExecution->send( - $this->activityResult($activityCompletion, $run), - $activityCompletion->recorded_at, - ); - } else { - $failureId = $activityCompletion->payload['failure_id'] ?? null; - - $current = $workflowExecution->throw( - $this->activityException($activityCompletion, null, $run), - $activityCompletion->recorded_at, - ); - - $this->recordFailureHandled( - $run, - $task, - is_string($failureId) ? $failureId : null, - $sequence, - $activityCompletion->payload, - ); - } - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - $run->load([ - 'instance', - 'activityExecutions', - 'timers', - 'failures', - 'tasks', - 'commands', - 'updates', - 'historyEvents', - 'childLinks.childRun.instance.currentRun', - 'childLinks.childRun.failures', - 'childLinks.childRun.historyEvents', - ]); - - ++$sequence; - continue; + $activityCompletion->payload + ); } + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + $run->load( + [ + 'instance', + 'activityExecutions', + 'timers', + 'failures', + 'tasks', + 'commands', + 'updates', + 'historyEvents', + 'childLinks.childRun.instance.currentRun', + 'childLinks.childRun.failures', + 'childLinks.childRun.historyEvents', + ] + ); + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - if ($current instanceof ActivityCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); - - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } + private function executeActivity( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var ActivityCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::ACTIVITY, [ + 'activity_type' => $current->activity, + ])) { + return null; + } + $activityCompletion = $this->activityCompletionEvent($run, $sequence); + $cancellationDelivery = $this->deliverCancellationAtCall( + $run, + $task, + $sequence, + 'activity', + $activityCompletion, + $workflowExecution + ); + if ($cancellationDelivery === CancellationDeliveryState::Waiting) { + return $this->waitForNextResumeSource($run, $task); + } + if ($cancellationDelivery === CancellationDeliveryState::Delivered) { + return $this->resumeCancellation($run, $task, $frame); + } + if ($activityCompletion !== null) { + return $this->resumeRecordedActivity($run, $task, $frame, $activityCompletion); + } + if ($this->activityOpenEvent($run, $sequence) !== null) { + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->waitForNextResumeSource($run, $task); + } + /** @var ActivityExecution|null $execution */ + $execution = $run->activityExecutions->firstWhere('sequence', $sequence); + if ($execution === null) { + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->scheduleActivityOrFailRun($run, $task, $sequence, $current); + } + if (in_array($execution->status, [ActivityStatus::Pending, ActivityStatus::Running], true)) { + if (! $this->ensureTypedStepHistoryRecorded($run, $task, $sequence, WorkflowStepHistory::ACTIVITY)) { + return null; + } + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->waitForNextResumeSource($run, $task); + } + if (! $this->ensureTypedStepHistoryRecorded($run, $task, $sequence, WorkflowStepHistory::ACTIVITY)) { + return null; + } + return $this->resumeActivityExecution($run, $task, $frame, $execution); + } - if (! $this->ensureStepHistoryCompatible( + private function resumeRecordedActivity( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + WorkflowHistoryEvent $activityCompletion, + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + $current = &$frame->current; + $sequence = &$frame->sequence; + $propagatedActivityFailure = null; + try { + $this->syncWorkflowCursor($workflow, $sequence + 1); + if ($activityCompletion->event_type === HistoryEventType::ActivityCompleted) { + $current = $workflowExecution->send( + $this->activityResult($activityCompletion, $run), + RedriveReplayClock::activityCompletedAt($activityCompletion) + ); + } else { + $failureId = $activityCompletion->payload['failure_id'] ?? null; + $propagatedActivityFailure = $this->activityException($activityCompletion, null, $run); + $current = $workflowExecution->throw($propagatedActivityFailure, $activityCompletion->recorded_at); + $this->recordFailureHandled( $run, $task, + is_string($failureId) ? $failureId : null, $sequence, - WorkflowStepHistory::ACTIVITY, - [ - 'activity_type' => $current->activity, - ], - )) { - return null; - } - - $activityCompletion = $this->activityCompletionEvent($run, $sequence); + $activityCompletion->payload + ); + } + } catch (Throwable $throwable) { + $this->failRun( + $run, + $task, + $throwable, + 'workflow_run', + $run->id, + $activityCompletion->event_type === HistoryEventType::ActivityFailed && $throwable === $propagatedActivityFailure ? $sequence : null + ); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - $cancellationDelivery = $this->deliverCancellationAtCall( + private function resumeActivityExecution( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ActivityExecution $execution, + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + $current = &$frame->current; + $sequence = &$frame->sequence; + $propagatedActivityFailure = null; + try { + $this->syncWorkflowCursor($workflow, $sequence + 1); + if ($execution->status === ActivityStatus::Completed) { + $current = $workflowExecution->send($execution->activityResult(), $execution->closed_at); + } else { + $failure = $run->failures->firstWhere('source_id', $execution->id); + $propagatedActivityFailure = $this->activityException(null, $execution, $run); + $current = $workflowExecution->throw($propagatedActivityFailure, $execution->closed_at); + $this->recordFailureHandled( $run, $task, + $failure instanceof WorkflowFailure ? $failure->id : null, $sequence, - 'activity', - $activityCompletion, - $workflowExecution + [ + 'source_kind' => 'activity_execution', + 'source_id' => $execution->id, + 'exception_class' => $failure?->exception_class, + 'message' => $failure?->message, + ] ); - if ($cancellationDelivery === CancellationDeliveryState::Waiting) { - return $this->waitForNextResumeSource($run, $task); - } - if ($cancellationDelivery === CancellationDeliveryState::Delivered) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->throw( - new WorkflowCancellationRequestedException( - cancellation: CooperativeCancellationDelivery::context($run), - ), - $run->cancellation_delivered_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - if ($activityCompletion !== null) { - $propagatedActivityFailure = null; - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - if ($activityCompletion->event_type === HistoryEventType::ActivityCompleted) { - $current = $workflowExecution->send( - $this->activityResult($activityCompletion, $run), - RedriveReplayClock::activityCompletedAt($activityCompletion), - ); - } else { - $failureId = $activityCompletion->payload['failure_id'] ?? null; - - $propagatedActivityFailure = $this->activityException($activityCompletion, null, $run); - $current = $workflowExecution->throw( - $propagatedActivityFailure, - $activityCompletion->recorded_at, - ); - - $this->recordFailureHandled( - $run, - $task, - is_string($failureId) ? $failureId : null, - $sequence, - $activityCompletion->payload, - ); - } - } catch (Throwable $throwable) { - $this->failRun( - $run, - $task, - $throwable, - 'workflow_run', - $run->id, - $activityCompletion->event_type === HistoryEventType::ActivityFailed - && $throwable === $propagatedActivityFailure ? $sequence : null, - ); - - return null; - } - - ++$sequence; - continue; - } + } + } catch (Throwable $throwable) { + $this->failRun( + $run, + $task, + $throwable, + 'workflow_run', + $run->id, + $execution->status === ActivityStatus::Failed && $throwable === $propagatedActivityFailure ? $sequence : null + ); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - if ($this->activityOpenEvent($run, $sequence) !== null) { - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->waitForNextResumeSource($run, $task); - } + private function executeConditionWait( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var AwaitCall|AwaitWithTimeoutCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + try { + ConditionWaits::assertReplayCompatible($run, $sequence, $current); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + $resolutionEvent = $this->conditionWaitResolutionEvent($run, $sequence); + $cancellationDelivery = $this->deliverCancellationAtCall( + $run, + $task, + $sequence, + 'condition', + $resolutionEvent, + $workflowExecution + ); + if ($cancellationDelivery === CancellationDeliveryState::Waiting) { + return $this->waitForNextResumeSource($run, $task); + } + if ($cancellationDelivery === CancellationDeliveryState::Delivered) { + return $this->resumeCancellation($run, $task, $frame); + } + if ($resolutionEvent !== null) { + return $this->resumeConditionResolution($run, $task, $frame, $resolutionEvent); + } + return $this->waitForCondition($run, $task, $frame); + } - /** @var ActivityExecution|null $execution */ - $execution = $run->activityExecutions->firstWhere('sequence', $sequence); + private function resumeConditionResolution( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + WorkflowHistoryEvent $resolutionEvent + ): ?WorkflowTaskStep { + try { + $this->syncWorkflowCursor($frame->workflow, $frame->sequence + 1); + $frame->current = $frame->execution->send( + $resolutionEvent->event_type === HistoryEventType::ConditionWaitSatisfied, + $resolutionEvent->recorded_at + ); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$frame->sequence; + return WorkflowTaskStep::ContinueExecution; + } - if ($execution === null) { - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->scheduleActivityOrFailRun($run, $task, $sequence, $current); - } + private function resumeTimedOutCondition( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + string $waitId, + ?WorkflowTimer $timeoutTimer, + ?WorkflowHistoryEvent $timeoutFiredEvent + ): ?WorkflowTaskStep { + $this->recordConditionWaitTimedOut( + $run, + $task, + $frame->sequence, + $waitId, + $timeoutTimer, + self::intValue($timeoutFiredEvent?->payload['delay_seconds'] ?? null) ?? $frame->current->seconds, + $frame->current->conditionKey, + $frame->current->conditionDefinitionFingerprint, + $this->stringValue($timeoutFiredEvent?->payload['timer_id'] ?? null) + ); + try { + $this->syncWorkflowCursor($frame->workflow, $frame->sequence + 1); + $frame->current = $frame->execution->send( + false, + $timeoutFiredEvent?->recorded_at ?? $timeoutTimer?->fired_at + ); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$frame->sequence; + return WorkflowTaskStep::ContinueExecution; + } - if (in_array($execution->status, [ActivityStatus::Pending, ActivityStatus::Running], true)) { - if (! $this->ensureTypedStepHistoryRecorded( - $run, - $task, - $sequence, - WorkflowStepHistory::ACTIVITY - )) { - return null; - } + private function resumeSatisfiedCondition( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + string $waitId, + ?WorkflowTimer $timeoutTimer + ): ?WorkflowTaskStep { + if ($timeoutTimer !== null) { + $this->cancelConditionTimeout($run, $task, $timeoutTimer); + } + $satisfiedEvent = $this->recordConditionWaitSatisfied( + $run, + $task, + $frame->sequence, + $waitId, + $timeoutTimer, + $frame->current + ); + try { + $this->syncWorkflowCursor($frame->workflow, $frame->sequence + 1); + $frame->current = $frame->execution->send(true, $satisfiedEvent->recorded_at); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$frame->sequence; + return WorkflowTaskStep::ContinueExecution; + } - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->waitForNextResumeSource($run, $task); - } - - if (! $this->ensureTypedStepHistoryRecorded($run, $task, $sequence, WorkflowStepHistory::ACTIVITY)) { - return null; - } - - $propagatedActivityFailure = null; - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - if ($execution->status === ActivityStatus::Completed) { - $current = $workflowExecution->send($execution->activityResult(), $execution->closed_at); - } else { - $failure = $run->failures - ->firstWhere('source_id', $execution->id); - - $propagatedActivityFailure = $this->activityException(null, $execution, $run); - $current = $workflowExecution->throw($propagatedActivityFailure, $execution->closed_at); - - $this->recordFailureHandled( - $run, - $task, - $failure instanceof WorkflowFailure ? $failure->id : null, - $sequence, - [ - 'source_kind' => 'activity_execution', - 'source_id' => $execution->id, - 'exception_class' => $failure?->exception_class, - 'message' => $failure?->message, - ], - ); - } - } catch (Throwable $throwable) { - $this->failRun( - $run, - $task, - $throwable, - 'workflow_run', - $run->id, - $execution->status === ActivityStatus::Failed - && $throwable === $propagatedActivityFailure ? $sequence : null, - ); - - return null; - } + private function resumeImmediateConditionTimeout( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + string $waitId + ): ?WorkflowTaskStep { + $timeoutTimer = $this->fireImmediateConditionTimeout($run, $task, $frame->sequence, $waitId, $frame->current); + $timedOutEvent = $this->recordConditionWaitTimedOut( + $run, + $task, + $frame->sequence, + $waitId, + $timeoutTimer, + $frame->current->seconds, + $frame->current->conditionKey, + $frame->current->conditionDefinitionFingerprint + ); + try { + $this->syncWorkflowCursor($frame->workflow, $frame->sequence + 1); + $frame->current = $frame->execution->send(false, $timedOutEvent->recorded_at); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$frame->sequence; + return WorkflowTaskStep::ContinueExecution; + } - ++$sequence; - continue; + private function waitForCondition( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame + ): WorkflowTask|WorkflowTaskStep|null { + $waitId = $this->conditionWaitId($run, $frame->sequence) ?? (string) Str::ulid(); + $this->recordConditionWaitOpened( + $run, + $task, + $frame->sequence, + $waitId, + $frame->current instanceof AwaitWithTimeoutCall ? $frame->current->seconds : null, + $frame->current->conditionKey, + $frame->current->conditionDefinitionFingerprint + ); + /** @var WorkflowTimer|null $timeoutTimer */ + $timeoutTimer = $frame->current instanceof AwaitWithTimeoutCall ? $run->timers->firstWhere( + 'sequence', + $frame->sequence + ) : null; + $timeoutScheduledEvent = $frame->current instanceof AwaitWithTimeoutCall ? $this->conditionTimeoutScheduledEvent( + $run, + $frame->sequence + ) : null; + $timeoutFiredEvent = $frame->current instanceof AwaitWithTimeoutCall ? $this->conditionTimeoutFiredEvent( + $run, + $frame->sequence + ) : null; + if ($frame->current instanceof AwaitWithTimeoutCall && ($timeoutFiredEvent !== null || $timeoutScheduledEvent === null && $timeoutTimer?->status === TimerStatus::Fired)) { + return $this->resumeTimedOutCondition($run, $task, $frame, $waitId, $timeoutTimer, $timeoutFiredEvent); + } + try { + $conditionSatisfied = ($frame->current->condition)() === true; + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + if ($conditionSatisfied) { + return $this->resumeSatisfiedCondition($run, $task, $frame, $waitId, $timeoutTimer); + } + if ($frame->current instanceof AwaitWithTimeoutCall) { + if ($frame->current->seconds === 0) { + return $this->resumeImmediateConditionTimeout($run, $task, $frame, $waitId); } + if ($timeoutTimer === null) { + $this->syncWorkflowCursor($frame->workflow, $frame->sequence + 1); + return $this->scheduleConditionTimeout($run, $task, $frame->sequence, $waitId, $frame->current); + } + } + $this->syncWorkflowCursor($frame->workflow, $frame->sequence + 1); + return $this->waitForNextResumeSource($run, $task, true); + } - if ($current instanceof AwaitCall || $current instanceof AwaitWithTimeoutCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); - - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } - - try { - ConditionWaits::assertReplayCompatible($run, $sequence, $current); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - $resolutionEvent = $this->conditionWaitResolutionEvent($run, $sequence); - - $cancellationDelivery = $this->deliverCancellationAtCall( - $run, - $task, - $sequence, - 'condition', - $resolutionEvent, - $workflowExecution - ); - if ($cancellationDelivery === CancellationDeliveryState::Waiting) { - return $this->waitForNextResumeSource($run, $task); - } - if ($cancellationDelivery === CancellationDeliveryState::Delivered) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->throw( - new WorkflowCancellationRequestedException( - cancellation: CooperativeCancellationDelivery::context($run), - ), - $run->cancellation_delivered_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - if ($resolutionEvent !== null) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send( - $resolutionEvent->event_type === HistoryEventType::ConditionWaitSatisfied, - $resolutionEvent->recorded_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } + private function executeSideEffect( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var SideEffectCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::SIDE_EFFECT)) { + return null; + } + $sideEffectEvent = $this->sideEffectEvent($run, $sequence); + try { + if ($sideEffectEvent === null) { + $sideEffectEvent = $this->recordSideEffect($run, $task, $sequence, $current); + } + $this->syncWorkflowCursor($workflow, $sequence + 1); + $current = $workflowExecution->send( + $this->sideEffectResult($sideEffectEvent, $run), + $sideEffectEvent->recorded_at, + advanceCancellationTime: false + ); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - ++$sequence; - continue; - } + private function executeVersion( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + VersionDecisions $versionDecisions, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var VersionCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + $versionMarkerEvent = $versionDecisions->markerAt($sequence); + try { + $resolution = $versionDecisions->resolve($run, $current, $sequence); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + // Only assert VERSION_MARKER history shape on paths that will + // occupy a history slot at this sequence (recorded / fresh). + // The legacy-default path records nothing and does not advance + // the workflow sequence — the next yield (activity, timer…) + // legitimately shares this slot, so checking for + // VERSION_MARKER-compatibility here would spuriously reject + // legacy replays that have since produced ACTIVITY/TIMER + // events at the same sequence. + if ($resolution->advancesSequence && ! $this->ensureStepHistoryCompatible( + $run, + $task, + $sequence, + WorkflowStepHistory::VERSION_MARKER, + [ + 'change_id' => $current->changeId, + ] + )) { + return null; + } + $version = $resolution->version; + if (! $resolution->advancesSequence) { + $versionMarkerEvent = null; + } + try { + if ($resolution->shouldRecordMarker) { + $versionMarkerEvent = $this->recordVersionMarker($run, $task, $sequence, $current, $version); + } + $this->syncWorkflowCursor($workflow, $sequence + ($resolution->advancesSequence ? 1 : 0)); + $current = $workflowExecution->send( + $current->resolveValue($version), + $versionMarkerEvent?->recorded_at, + advanceCancellationTime: false + ); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + if ($resolution->advancesSequence) { + ++$sequence; + } + return WorkflowTaskStep::ContinueExecution; + } - $waitId = $this->conditionWaitId($run, $sequence) ?? (string) Str::ulid(); + private function executeSearchAttributes( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var UpsertSearchAttributesCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + if (! $this->ensureStepHistoryCompatible( + $run, + $task, + $sequence, + WorkflowStepHistory::SEARCH_ATTRIBUTES_UPSERT + )) { + return null; + } + $upsertEvent = $this->searchAttributesUpsertedEvent($run, $sequence); + try { + if ($upsertEvent === null) { + $upsertEvent = $this->recordSearchAttributesUpserted($run, $task, $sequence, $current); + } else { + SearchAttributeReplayIdentity::assertCompatible($sequence, $upsertEvent->payload, $current); + } + $this->syncWorkflowCursor($workflow, $sequence + 1); + $current = $workflowExecution->send(null, $upsertEvent?->recorded_at, advanceCancellationTime: false); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - $this->recordConditionWaitOpened( - $run, - $task, + private function executeMemo( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var UpsertMemoCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::MEMO_UPSERT)) { + return null; + } + $memoEvent = $this->memoUpsertedEvent($run, $sequence); + try { + if ($memoEvent === null) { + $memoEvent = $this->recordMemoUpserted($run, $task, $sequence, $current); + } else { + MemoReplayIdentity::assertCompatible( $sequence, - $waitId, - $current instanceof AwaitWithTimeoutCall ? $current->seconds : null, - $current->conditionKey, - $current->conditionDefinitionFingerprint, + $memoEvent->payload['entries'] ?? null, + $current->entries ); + } + $this->syncWorkflowCursor($workflow, $sequence + 1); + $current = $workflowExecution->send(null, $memoEvent?->recorded_at, advanceCancellationTime: false); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - /** @var WorkflowTimer|null $timeoutTimer */ - $timeoutTimer = $current instanceof AwaitWithTimeoutCall - ? $run->timers->firstWhere('sequence', $sequence) - : null; - $timeoutScheduledEvent = $current instanceof AwaitWithTimeoutCall - ? $this->conditionTimeoutScheduledEvent($run, $sequence) - : null; - $timeoutFiredEvent = $current instanceof AwaitWithTimeoutCall - ? $this->conditionTimeoutFiredEvent($run, $sequence) - : null; - - if ( - $current instanceof AwaitWithTimeoutCall - && ( - $timeoutFiredEvent !== null - || ($timeoutScheduledEvent === null && $timeoutTimer?->status === TimerStatus::Fired) - ) - ) { - $this->recordConditionWaitTimedOut( - $run, - $task, - $sequence, - $waitId, - $timeoutTimer, - self::intValue($timeoutFiredEvent?->payload['delay_seconds'] ?? null) ?? $current->seconds, - $current->conditionKey, - $current->conditionDefinitionFingerprint, - $this->stringValue($timeoutFiredEvent?->payload['timer_id'] ?? null), - ); - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send( - false, - $timeoutFiredEvent?->recorded_at ?? $timeoutTimer?->fired_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - try { - $conditionSatisfied = ($current->condition)() === true; - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - if ($conditionSatisfied) { - if ($timeoutTimer !== null) { - $this->cancelConditionTimeout($run, $task, $timeoutTimer); - } - - $satisfiedEvent = $this->recordConditionWaitSatisfied( - $run, - $task, - $sequence, - $waitId, - $timeoutTimer, - $current - ); - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send(true, $satisfiedEvent->recorded_at); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } + private function executeTimer( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var TimerCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::TIMER)) { + return null; + } + $timerFired = $this->timerFiredEvent($run, $sequence); + $cancellationDelivery = $this->deliverCancellationAtCall( + $run, + $task, + $sequence, + 'timer', + $timerFired, + $workflowExecution + ); + if ($cancellationDelivery === CancellationDeliveryState::Waiting) { + return $this->waitForNextResumeSource($run, $task); + } + if ($cancellationDelivery === CancellationDeliveryState::Delivered) { + return $this->resumeCancellation($run, $task, $frame); + } + if ($timerFired !== null) { + return $this->resumeTimer($run, $task, $frame, $timerFired); + } + if ($this->timerScheduledEvent($run, $sequence) !== null) { + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->waitForNextResumeSource($run, $task); + } + /** @var WorkflowTimer|null $timer */ + $timer = $run->timers->firstWhere('sequence', $sequence); + if ($timer === null) { + if ($current->seconds === 0) { + $fired = $this->fireImmediateTimer($run, $task, $sequence, $current); + return $this->resumeTimer($run, $task, $frame, $fired); + } + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->scheduleTimer($run, $task, $sequence, $current); + } + if ($timer->status === TimerStatus::Pending) { + if (! $this->ensureTypedStepHistoryRecorded($run, $task, $sequence, WorkflowStepHistory::TIMER)) { + return null; + } + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->waitForNextResumeSource($run, $task); + } + if (! $this->ensureTypedStepHistoryRecorded($run, $task, $sequence, WorkflowStepHistory::TIMER)) { + return null; + } + return $this->resumeTimer($run, $task, $frame, $timer); + } - if ($current instanceof AwaitWithTimeoutCall) { - if ($current->seconds === 0) { - $timeoutTimer = $this->fireImmediateConditionTimeout( - $run, - $task, - $sequence, - $waitId, - $current, - ); - - $timedOutEvent = $this->recordConditionWaitTimedOut( - $run, - $task, - $sequence, - $waitId, - $timeoutTimer, - $current->seconds, - $current->conditionKey, - $current->conditionDefinitionFingerprint, - ); - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send(false, $timedOutEvent->recorded_at); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } + private function resumeTimer( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + WorkflowHistoryEvent|WorkflowTimer $timer, + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + $current = &$frame->current; + $sequence = &$frame->sequence; + try { + $this->syncWorkflowCursor($workflow, $sequence + 1); + $current = $workflowExecution->send( + true, + $timer instanceof WorkflowHistoryEvent ? $timer->recorded_at : $timer->fired_at, + ); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - if ($timeoutTimer === null) { - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->scheduleConditionTimeout($run, $task, $sequence, $waitId, $current); - } - } + private function executeSignal( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var SignalCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::SIGNAL_WAIT, [ + 'signal_name' => $current->name, + ])) { + return null; + } + $signalEvent = $this->appliedSignalEvent($run, $sequence, $current); + $cancellationDelivery = $this->deliverCancellationAtCall( + $run, + $task, + $sequence, + 'signal', + $signalEvent, + $workflowExecution + ); + if ($cancellationDelivery === CancellationDeliveryState::Waiting) { + return $this->waitForNextResumeSource($run, $task); + } + if ($cancellationDelivery === CancellationDeliveryState::Delivered) { + return $this->resumeCancellation($run, $task, $frame); + } + if ($signalEvent !== null) { + return $this->resumeSignal($run, $task, $frame, $signalEvent); + } + /** @var WorkflowTimer|null $timeoutTimer */ + $timeoutTimer = $current->timeoutSeconds !== null ? $run->timers->firstWhere('sequence', $sequence) : null; + $timeoutScheduledEvent = $current->timeoutSeconds !== null ? $this->signalTimeoutScheduledEvent( + $run, + $sequence, + $current->name + ) : null; + $timeoutFiredEvent = $current->timeoutSeconds !== null ? $this->signalTimeoutFiredEvent( + $run, + $sequence, + $current->name + ) : null; + $signalWaitId = $this->signalWaitId($run, $sequence, $current) ?? (string) Str::ulid(); + $signalCommand = $this->pendingSignalCommand($run, $current); + $timeoutHasWon = $this->signalTimeoutHasWon( + $run, + $current->timeoutSeconds !== null, + $timeoutTimer, + $timeoutScheduledEvent, + $timeoutFiredEvent, + $signalCommand, + ); + if ($timeoutHasWon) { + $this->recordSignalWait($run, $task, $sequence, $current, $signalWaitId); + return $this->resumeSignalTimeout($run, $task, $frame, $timeoutFiredEvent, $timeoutTimer); + } + if ($signalCommand !== null) { + $signalWaitId = $this->signalWaitIdForCommand($run, $signalCommand, $current->name); + $this->recordSignalWait($run, $task, $sequence, $current, $signalWaitId); + if ($timeoutTimer !== null) { + $this->cancelSignalTimeout($run, $task, $timeoutTimer); + } + $signalEvent = $this->applySignal($run, $task, $sequence, $current, $signalCommand, $signalWaitId); + return $this->resumeSignal($run, $task, $frame, $signalEvent); + } + return $this->waitForSignal($run, $task, $frame, $signalWaitId, $timeoutTimer); + } + private function waitForSignal( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + string $signalWaitId, + ?WorkflowTimer $timeoutTimer, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + /** @var SignalCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + $this->recordSignalWait($run, $task, $sequence, $current, $signalWaitId); + if ($current->timeoutSeconds !== null) { + if ($current->timeoutSeconds === 0) { + $firedTimer = $this->fireImmediateSignalTimeout($run, $task, $sequence, $signalWaitId, $current); + return $this->resumeSignalTimeout($run, $task, $frame, null, $firedTimer); + } + if ($timeoutTimer === null) { $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->waitForNextResumeSource($run, $task, true); + return $this->scheduleSignalTimeout($run, $task, $sequence, $signalWaitId, $current); } + } + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->waitForNextResumeSource($run, $task, true); + } - if ($current instanceof SideEffectCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); - - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } - - if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::SIDE_EFFECT)) { - return null; - } - - $sideEffectEvent = $this->sideEffectEvent($run, $sequence); - - try { - if ($sideEffectEvent === null) { - $sideEffectEvent = $this->recordSideEffect($run, $task, $sequence, $current); - } - - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send( - $this->sideEffectResult($sideEffectEvent, $run), - $sideEffectEvent->recorded_at, - advanceCancellationTime: false, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; + private function signalTimeoutHasWon( + WorkflowRun $run, + bool $hasTimeout, + ?WorkflowTimer $timeoutTimer, + ?WorkflowHistoryEvent $timeoutScheduledEvent, + ?WorkflowHistoryEvent $timeoutFiredEvent, + ?WorkflowCommand $signalCommand, + ): bool { + $timeoutHasWon = $hasTimeout && ($timeoutFiredEvent !== null || $timeoutScheduledEvent === null && $timeoutTimer?->status === TimerStatus::Fired); + if ($timeoutHasWon && $signalCommand !== null && $timeoutFiredEvent !== null) { + $signalReceivedEvent = $this->signalReceivedEventForCommand($run, $signalCommand); + if ($signalReceivedEvent !== null && $signalReceivedEvent->sequence < $timeoutFiredEvent->sequence) { + $timeoutHasWon = false; } + } + return $timeoutHasWon; + } - if ($current instanceof VersionCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + private function resumeSignal( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + WorkflowHistoryEvent $signalEvent, + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + $current = &$frame->current; + $sequence = &$frame->sequence; + try { + $this->syncWorkflowCursor($workflow, $sequence + 1); + $current = $workflowExecution->send($this->signalValue($signalEvent, $run), $signalEvent->recorded_at); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } + private function resumeSignalTimeout( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ?WorkflowHistoryEvent $timeoutFiredEvent, + ?WorkflowTimer $timeoutTimer, + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + $current = &$frame->current; + $sequence = &$frame->sequence; + try { + $this->syncWorkflowCursor($workflow, $sequence + 1); + $current = $workflowExecution->send(null, $timeoutFiredEvent?->recorded_at ?? $timeoutTimer?->fired_at); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - $versionMarkerEvent = $versionDecisions->markerAt($sequence); - - try { - $resolution = $versionDecisions->resolve($run, $current, $sequence); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - // Only assert VERSION_MARKER history shape on paths that will - // occupy a history slot at this sequence (recorded / fresh). - // The legacy-default path records nothing and does not advance - // the workflow sequence — the next yield (activity, timer…) - // legitimately shares this slot, so checking for - // VERSION_MARKER-compatibility here would spuriously reject - // legacy replays that have since produced ACTIVITY/TIMER - // events at the same sequence. - if ($resolution->advancesSequence - && ! $this->ensureStepHistoryCompatible( - $run, - $task, - $sequence, - WorkflowStepHistory::VERSION_MARKER, - [ - 'change_id' => $current->changeId, - ], - ) - ) { - return null; - } - - $version = $resolution->version; - if (! $resolution->advancesSequence) { - $versionMarkerEvent = null; - } - - try { - if ($resolution->shouldRecordMarker) { - $versionMarkerEvent = $this->recordVersionMarker($run, $task, $sequence, $current, $version); - } - - $this->syncWorkflowCursor($workflow, $sequence + ($resolution->advancesSequence ? 1 : 0)); - $current = $workflowExecution->send( - $current->resolveValue($version), - $versionMarkerEvent?->recorded_at, - advanceCancellationTime: false, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - if ($resolution->advancesSequence) { - ++$sequence; - } - - continue; + private function executeServiceOperation( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var ServiceOperationCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::SERVICE_OPERATION, [ + 'operation_name' => $current->operationName, + ])) { + return null; + } + $serviceEvent = $this->serviceOperationEvent($run, $sequence); + if (! self::serviceOperationResumesOnAdmission($current)) { + $cancellationDelivery = $this->deliverCancellationAtCall( + $run, + $task, + $sequence, + 'service_operation', + $serviceEvent?->event_type === HistoryEventType::ServiceCallStarted ? null : $serviceEvent, + $workflowExecution + ); + if ($cancellationDelivery === CancellationDeliveryState::Waiting) { + return $this->waitForNextResumeSource($run, $task); } + if ($cancellationDelivery === CancellationDeliveryState::Delivered) { + return $this->resumeCancellation($run, $task, $frame); + } + } + if ($serviceEvent === null) { + try { + $serviceEvent = $this->recordServiceOperationEvent($run, $task, $sequence, $current); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + } + if ($serviceEvent->event_type === HistoryEventType::ServiceCallStarted && ! self::serviceOperationStartedEventIsVisible( + $serviceEvent, + $current + )) { + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->waitForNextResumeSource($run, $task); + } + return $this->resumeServiceOperation($run, $task, $frame, $serviceEvent); + } - if ($current instanceof UpsertSearchAttributesCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + private function resumeServiceOperation( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + WorkflowHistoryEvent $serviceEvent, + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + $current = &$frame->current; + $sequence = &$frame->sequence; + try { + $this->syncWorkflowCursor($workflow, $sequence + 1); + if (in_array( + $serviceEvent->event_type, + [HistoryEventType::ServiceCallFailed, HistoryEventType::ServiceCallCancelled], + true + )) { + $current = $workflowExecution->throw( + $this->serviceOperationException($serviceEvent), + $serviceEvent->recorded_at + ); + $this->recordFailureHandled($run, $task, null, $sequence, $serviceEvent->payload); + } else { + $current = $workflowExecution->send( + $this->serviceOperationResult($serviceEvent, $run), + $serviceEvent->recorded_at + ); + } + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } + private function executeChild( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var ChildWorkflowCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::CHILD_WORKFLOW, [ + 'child_workflow_type' => $current->workflow, + ])) { + return null; + } + $resolutionEvent = ChildRunHistory::resolutionEventForSequence($run, $sequence); + $childRun = ChildRunHistory::childRunForSequence($run, $sequence); + $cancellationDelivery = $this->deliverCancellationAtCall( + $run, + $task, + $sequence, + 'child', + $resolutionEvent, + $workflowExecution + ); + if ($cancellationDelivery === CancellationDeliveryState::Waiting) { + return $this->waitForNextResumeSource($run, $task); + } + if ($cancellationDelivery === CancellationDeliveryState::Delivered) { + return $this->resumeCancellation($run, $task, $frame); + } + if ($resolutionEvent !== null) { + return $this->resumeChildResolution($run, $task, $frame, $resolutionEvent, $childRun); + } + if ($childRun === null) { + if (ChildRunHistory::scheduledEventForSequence( + $run, + $sequence + ) !== null || ChildRunHistory::startedEventForSequence($run, $sequence) !== null) { + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->waitForNextResumeSource($run, $task); + } + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->scheduleChildWorkflowOrFailRun($run, $task, $sequence, $current); + } + $childStatus = ChildRunHistory::resolvedStatus(null, $childRun); + if (in_array($childStatus, [RunStatus::Pending, RunStatus::Running, RunStatus::Waiting], true)) { + if (! $this->ensureTypedStepHistoryRecorded($run, $task, $sequence, WorkflowStepHistory::CHILD_WORKFLOW)) { + return null; + } + $this->syncWorkflowCursor($workflow, $sequence + 1); + return $this->waitForNextResumeSource($run, $task); + } + if (! $this->ensureTypedStepHistoryRecorded($run, $task, $sequence, WorkflowStepHistory::CHILD_WORKFLOW)) { + return null; + } + $resolutionEvent = $this->recordChildResolution($run, $task, $sequence, $childRun); + return $this->resumeChildResolution($run, $task, $frame, $resolutionEvent, $childRun); + } - if (! $this->ensureStepHistoryCompatible( + private function resumeChildResolution( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + WorkflowHistoryEvent $resolutionEvent, + ?WorkflowRun $childRun, + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + $current = &$frame->current; + $sequence = &$frame->sequence; + try { + $this->syncWorkflowCursor($workflow, $sequence + 1); + if ($resolutionEvent->event_type === HistoryEventType::ChildRunCompleted) { + $current = $workflowExecution->send( + ChildRunHistory::outputForResolution($resolutionEvent, $childRun), + $resolutionEvent->recorded_at + ); + } else { + $failureId = $resolutionEvent->payload['failure_id'] ?? null; + $current = $workflowExecution->throw( + ChildRunHistory::exceptionForResolution($resolutionEvent, $childRun), + $resolutionEvent->recorded_at + ); + $this->recordFailureHandled( $run, $task, + is_string($failureId) ? $failureId : null, $sequence, - WorkflowStepHistory::SEARCH_ATTRIBUTES_UPSERT - )) { - return null; - } - - $upsertEvent = $this->searchAttributesUpsertedEvent($run, $sequence); - - try { - if ($upsertEvent === null) { - $upsertEvent = $this->recordSearchAttributesUpserted($run, $task, $sequence, $current); - } else { - SearchAttributeReplayIdentity::assertCompatible( - $sequence, - $upsertEvent->payload, - $current, - ); - } - - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send( - null, - $upsertEvent?->recorded_at, - advanceCancellationTime: false - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; + $this->childFailureHandledPayload($resolutionEvent) + ); } + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + ++$sequence; + return WorkflowTaskStep::ContinueExecution; + } - if ($current instanceof UpsertMemoCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); - - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } - - if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::MEMO_UPSERT)) { - return null; - } - - $memoEvent = $this->memoUpsertedEvent($run, $sequence); - - try { - if ($memoEvent === null) { - $memoEvent = $this->recordMemoUpserted($run, $task, $sequence, $current); - } else { - MemoReplayIdentity::assertCompatible( - $sequence, - $memoEvent->payload['entries'] ?? null, - $current->entries, - ); - } - - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send(null, $memoEvent?->recorded_at, advanceCancellationTime: false); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; + private function executeAll( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + ): WorkflowTask|WorkflowTaskStep|null { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var AllCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + + $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); + if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { + return $this->restartAfterPendingUpdateFailure($run, $task); + } + $leafDescriptors = $current->leafDescriptors($sequence); + $groupSize = count($leafDescriptors); + try { + StructuralLimits::guardCommandBatchSize($groupSize); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + if ($groupSize === 0) { + try { + $this->syncWorkflowCursor($workflow, $sequence); + $current = $workflowExecution->send($current->nestedResults([])); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; } + return WorkflowTaskStep::ContinueExecution; + } + foreach ($leafDescriptors as $descriptor) { + $call = $descriptor['call']; + $itemSequence = $sequence + $descriptor['offset']; + if (! $this->ensureStepHistoryCompatible($run, $task, $itemSequence, match (true) { + $call instanceof ActivityCall => WorkflowStepHistory::ACTIVITY, + $call instanceof TimerCall => WorkflowStepHistory::TIMER, + $call instanceof SignalCall => WorkflowStepHistory::SIGNAL_WAIT, + $call instanceof AwaitCall, $call instanceof AwaitWithTimeoutCall => WorkflowStepHistory::CONDITION_WAIT, + default => WorkflowStepHistory::CHILD_WORKFLOW, + }, $this->parallelMemberIdentity($call))) { + return null; + } + } + if (! $this->ensureParallelGroupHistoryCompatible($run, $task, $sequence, $leafDescriptors)) { + return null; + } + $groupResolution = $this->parallelResolutionEvent($run, $current, $sequence, $leafDescriptors); + $cancellationDelivery = $this->deliverCancellationAtCall( + $run, + $task, + $sequence, + 'parallel', + $groupResolution, + $workflowExecution, + $current + ); + if ($cancellationDelivery === CancellationDeliveryState::Waiting) { + return $this->waitForNextResumeSource($run, $task); + } + if ($cancellationDelivery === CancellationDeliveryState::Delivered) { + return $this->resumeCancellation($run, $task, $frame, $groupSize); + } + $group = new WorkflowParallelDispatch($sequence); + foreach ($leafDescriptors as $descriptor) { + if (! $this->dispatchParallelMember($run, $task, $descriptor, $group)) { + return null; + } + } + return $this->finishParallelDispatch($run, $task, $frame, $leafDescriptors, $group); + } - if ($current instanceof TimerCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); - - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } - - if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::TIMER)) { - return null; - } - - $timerFired = $this->timerFiredEvent($run, $sequence); + private function parallelMemberIdentity(mixed $call): array + { + return match (true) { + $call instanceof ActivityCall => [ + 'activity_type' => $call->activity, + ], + $call instanceof ChildWorkflowCall => [ + 'child_workflow_type' => $call->workflow, + ], + default => [], + }; + } - $cancellationDelivery = $this->deliverCancellationAtCall( - $run, - $task, - $sequence, - 'timer', - $timerFired, - $workflowExecution + private function finishParallelDispatch( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + array $leafDescriptors, + WorkflowParallelDispatch $group + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var AllCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + $groupSize = count($leafDescriptors); + if ($group->scheduledTasks !== []) { + $this->logApproachingLimit(StructuralLimits::warnApproachingCommandBatch($groupSize), $run); + } + if ($current instanceof SelectCall) { + $groupId = ParallelChildGroup::payloadForPath( + $leafDescriptors[0]['group_path'] + )['parallel_group_path'][0]['parallel_group_id']; + $selection = ParallelChildGroup::selectionResolution($run, $groupId); + if ($selection instanceof WorkflowHistoryEvent) { + return $this->resumeParallelSelection($run, $task, $frame, $groupSize, $group, $selection); + } + } elseif ($group->failure !== null) { + return $this->resumeParallelFailure($run, $task, $frame, $groupSize, $group); + } + if (! $group->pending) { + ksort($group->results); + try { + $this->syncWorkflowCursor($workflow, $sequence + $groupSize); + $current = $workflowExecution->send( + $current->nestedResults(array_values($group->results)), + $group->successTime ); - if ($cancellationDelivery === CancellationDeliveryState::Waiting) { - return $this->waitForNextResumeSource($run, $task); - } - if ($cancellationDelivery === CancellationDeliveryState::Delivered) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->throw( - new WorkflowCancellationRequestedException( - cancellation: CooperativeCancellationDelivery::context($run), - ), - $run->cancellation_delivered_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + $sequence += $groupSize; + return WorkflowTaskStep::ContinueExecution; + } + $this->markRunWaiting($run, $task); + foreach ($group->scheduledTasks as $scheduledTask) { + TaskDispatcher::dispatch($scheduledTask); + } + $this->syncWorkflowCursor($workflow, $sequence + $groupSize); + return null; + } - if ($timerFired !== null) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send(true, $timerFired->recorded_at); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + private function resumeParallelSelection( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + int $groupSize, + WorkflowParallelDispatch $group, + WorkflowHistoryEvent $selection, + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + /** @var SelectCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + try { + $winner = ParallelChildGroup::validatedSelectionResolution($run, $current, $sequence, $selection); + $this->syncWorkflowCursor($workflow, $sequence + $groupSize); + $current = $workflowExecution->send( + $current->resolved( + $sequence, + $winner, + $group->results, + $group->failures, + ParallelChildGroup::durableOperationIdentities($run) + ), + $selection->recorded_at + ); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + $sequence += $groupSize; + return WorkflowTaskStep::ContinueExecution; + } - return null; - } + private function resumeParallelFailure( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + int $groupSize, + WorkflowParallelDispatch $group, + ): ?WorkflowTaskStep { + $workflow = $frame->workflow; + $workflowExecution = $frame->execution; + $current = &$frame->current; + $sequence = &$frame->sequence; + try { + $this->syncWorkflowCursor($workflow, $sequence + $groupSize); + $failureTime = isset($group->failure['recorded_at']) && is_int( + $group->failure['recorded_at'] + ) && $group->failure['recorded_at'] !== PHP_INT_MAX ? \Carbon\Carbon::createFromTimestampMs( + $group->failure['recorded_at'] + ) : null; + $current = $workflowExecution->throw($group->failure['exception'], $failureTime); + $this->recordFailureHandled( + $run, + $task, + is_string($group->failure['failure_id'] ?? null) ? $group->failure['failure_id'] : null, + $sequence + (int) $group->failure['index'], + is_array($group->failure['failure_payload'] ?? null) ? $group->failure['failure_payload'] : [] + ); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; + } + $sequence += $groupSize; + return WorkflowTaskStep::ContinueExecution; + } - ++$sequence; - continue; - } + private function dispatchParallelMember( + WorkflowRun $run, + WorkflowTask $task, + array $descriptor, + WorkflowParallelDispatch $group, + ): bool { + return match (true) { + $descriptor['call'] instanceof ActivityCall => $this->dispatchParallelActivity( + $run, + $task, + $descriptor, + $group + ), + $descriptor['call'] instanceof TimerCall => $this->dispatchParallelTimer($run, $task, $descriptor, $group), + $descriptor['call'] instanceof AwaitCall || $descriptor['call'] instanceof AwaitWithTimeoutCall => $this->dispatchParallelCondition( + $run, + $task, + $descriptor, + $group + ), + $descriptor['call'] instanceof SignalCall => $this->dispatchParallelSignal( + $run, + $task, + $descriptor, + $group + ), + default => $this->dispatchParallelChild($run, $task, $descriptor, $group), + }; + } - if ($this->timerScheduledEvent($run, $sequence) !== null) { - $this->syncWorkflowCursor($workflow, $sequence + 1); + private function dispatchParallelActivity( + WorkflowRun $run, + WorkflowTask $task, + array $descriptor, + WorkflowParallelDispatch $group + ): bool { + $call = $descriptor['call']; + $offset = $descriptor['offset']; + $itemSequence = $group->baseSequence + $offset; + $parallelMetadata = ParallelChildGroup::payloadForPath($descriptor['group_path']); + $activityCompletion = $this->activityCompletionEvent($run, $itemSequence); + if ($activityCompletion !== null) { + if ($activityCompletion->event_type === HistoryEventType::ActivityCompleted) { + $group->results[$offset] = $this->activityResult($activityCompletion, $run); + $group->successTime = $this->latestReplayTime( + $group->successTime, + $activityCompletion->recorded_at ?? $activityCompletion->created_at + ); + return true; + } + $group->failure = ParallelFailureSelector::select( + $group->failure, + $offset, + $this->activityException($activityCompletion, null, $run), + $activityCompletion->recorded_at?->getTimestampMs() ?? $activityCompletion->created_at?->getTimestampMs() ?? PHP_INT_MAX, + is_string( + $activityCompletion->payload['failure_id'] ?? null + ) ? $activityCompletion->payload['failure_id'] : null, + $activityCompletion->payload + ); + $group->failures[$offset] = $this->activityException($activityCompletion, null, $run); + return true; + } + if ($this->activityOpenEvent($run, $itemSequence) !== null) { + $group->pending = true; + return true; + } + /** @var ActivityExecution|null $execution */ + $execution = $run->activityExecutions->firstWhere('sequence', $itemSequence); + if (! $execution instanceof ActivityExecution) { + $activityTask = $this->scheduleActivityOrFailRun( + $run, + $task, + $itemSequence, + $call, + $parallelMetadata, + false + ); + if (! $activityTask instanceof WorkflowTask) { + return false; + } + $group->scheduledTasks[] = $activityTask; + $group->pending = true; + return true; + } + if (in_array($execution->status, [ActivityStatus::Pending, ActivityStatus::Running], true)) { + if (! $this->ensureTypedStepHistoryRecorded($run, $task, $itemSequence, WorkflowStepHistory::ACTIVITY)) { + return false; + } + $group->pending = true; + return true; + } + if (! $this->ensureTypedStepHistoryRecorded($run, $task, $itemSequence, WorkflowStepHistory::ACTIVITY)) { + return false; + } + if ($execution->status === ActivityStatus::Completed) { + $group->results[$offset] = $execution->activityResult(); + $group->successTime = $this->latestReplayTime( + $group->successTime, + $execution->closed_at ?? $execution->updated_at ?? $execution->created_at + ); + return true; + } + /** @var WorkflowFailure|null $activityFailure */ + $activityFailure = $run->failures->firstWhere('source_id', $execution->id); + $group->failure = ParallelFailureSelector::select( + $group->failure, + $offset, + $this->activityException(null, $execution, $run), + $execution->closed_at?->getTimestampMs() ?? PHP_INT_MAX, + $activityFailure?->id, + [ + 'source_kind' => 'activity_execution', + 'source_id' => $execution->id, + 'exception_class' => $activityFailure?->exception_class, + 'message' => $activityFailure?->message, - return $this->waitForNextResumeSource($run, $task); - } + ] + ); + $group->failures[$offset] = $this->activityException(null, $execution, $run); + return true; + } - /** @var WorkflowTimer|null $timer */ - $timer = $run->timers->firstWhere('sequence', $sequence); - - if ($timer === null) { - if ($current->seconds === 0) { - $fired = $this->fireImmediateTimer($run, $task, $sequence, $current); - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send(true, $fired->recorded_at); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - - continue; - } - - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->scheduleTimer($run, $task, $sequence, $current); - } - - if ($timer->status === TimerStatus::Pending) { - if (! $this->ensureTypedStepHistoryRecorded($run, $task, $sequence, WorkflowStepHistory::TIMER)) { - return null; - } - - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->waitForNextResumeSource($run, $task); - } - - if (! $this->ensureTypedStepHistoryRecorded($run, $task, $sequence, WorkflowStepHistory::TIMER)) { - return null; - } - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send(true, $timer->fired_at); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - if ($current instanceof SignalCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); - - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } - - if (! $this->ensureStepHistoryCompatible( - $run, - $task, - $sequence, - WorkflowStepHistory::SIGNAL_WAIT, - [ - 'signal_name' => $current->name, - ], - )) { - return null; - } - - $signalEvent = $this->appliedSignalEvent($run, $sequence, $current); - - $cancellationDelivery = $this->deliverCancellationAtCall( - $run, - $task, - $sequence, - 'signal', - $signalEvent, - $workflowExecution - ); - if ($cancellationDelivery === CancellationDeliveryState::Waiting) { - return $this->waitForNextResumeSource($run, $task); - } - if ($cancellationDelivery === CancellationDeliveryState::Delivered) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->throw( - new WorkflowCancellationRequestedException( - cancellation: CooperativeCancellationDelivery::context($run), - ), - $run->cancellation_delivered_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - if ($signalEvent !== null) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send( - $this->signalValue($signalEvent, $run), - $signalEvent->recorded_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - /** @var WorkflowTimer|null $timeoutTimer */ - $timeoutTimer = $current->timeoutSeconds !== null - ? $run->timers->firstWhere('sequence', $sequence) - : null; - $timeoutScheduledEvent = $current->timeoutSeconds !== null - ? $this->signalTimeoutScheduledEvent($run, $sequence, $current->name) - : null; - $timeoutFiredEvent = $current->timeoutSeconds !== null - ? $this->signalTimeoutFiredEvent($run, $sequence, $current->name) - : null; - $signalWaitId = $this->signalWaitId($run, $sequence, $current) ?? (string) Str::ulid(); - $signalCommand = $this->pendingSignalCommand($run, $current); - - $timeoutHasWon = ( - $current->timeoutSeconds !== null - && ( - $timeoutFiredEvent !== null - || ($timeoutScheduledEvent === null && $timeoutTimer?->status === TimerStatus::Fired) - ) + private function dispatchParallelTimer( + WorkflowRun $run, + WorkflowTask $task, + array $descriptor, + WorkflowParallelDispatch $group + ): bool { + $call = $descriptor['call']; + $offset = $descriptor['offset']; + $itemSequence = $group->baseSequence + $offset; + $parallelMetadata = ParallelChildGroup::payloadForPath($descriptor['group_path']); + $timerFired = $this->timerFiredEvent($run, $itemSequence); + if ($timerFired !== null) { + $group->results[$offset] = true; + $group->successTime = $this->latestReplayTime( + $group->successTime, + $timerFired->recorded_at ?? $timerFired->created_at + ); + return true; + } + if ($this->timerScheduledEvent($run, $itemSequence) !== null) { + $group->pending = true; + return true; + } + /** @var WorkflowTimer|null $timer */ + $timer = $run->timers->firstWhere('sequence', $itemSequence); + if ($timer instanceof WorkflowTimer) { + if ($timer->status === TimerStatus::Fired) { + $group->results[$offset] = true; + $group->successTime = $this->latestReplayTime( + $group->successTime, + $timer->fired_at ?? $timer->updated_at ?? $timer->created_at ); - - if ($timeoutHasWon && $signalCommand !== null && $timeoutFiredEvent !== null) { - $signalReceivedEvent = $this->signalReceivedEventForCommand($run, $signalCommand); - - if ( - $signalReceivedEvent !== null - && $signalReceivedEvent->sequence < $timeoutFiredEvent->sequence - ) { - $timeoutHasWon = false; - } - } - - if ($timeoutHasWon) { - $this->recordSignalWait($run, $task, $sequence, $current, $signalWaitId); - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send( - null, - $timeoutFiredEvent?->recorded_at ?? $timeoutTimer?->fired_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - if ($signalCommand !== null) { - $signalWaitId = $this->signalWaitIdForCommand($run, $signalCommand, $current->name); - - $this->recordSignalWait($run, $task, $sequence, $current, $signalWaitId); - - if ($timeoutTimer !== null) { - $this->cancelSignalTimeout($run, $task, $timeoutTimer); - } - - $signalEvent = $this->applySignal( - $run, - $task, - $sequence, - $current, - $signalCommand, - $signalWaitId, - ); - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send( - $this->signalValue($signalEvent, $run), - $signalEvent?->recorded_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - $this->recordSignalWait($run, $task, $sequence, $current, $signalWaitId); - - if ($current->timeoutSeconds !== null) { - if ($current->timeoutSeconds === 0) { - $firedTimer = $this->fireImmediateSignalTimeout( - $run, - $task, - $sequence, - $signalWaitId, - $current - ); - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->send(null, $firedTimer->fired_at); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - if ($timeoutTimer === null) { - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->scheduleSignalTimeout($run, $task, $sequence, $signalWaitId, $current); - } - } - - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->waitForNextResumeSource($run, $task, true); + } else { + $group->pending = true; } + return true; + } + if ($call->seconds === 0) { + $fired = $this->fireImmediateTimer($run, $task, $itemSequence, $call, $parallelMetadata); + $group->results[$offset] = true; + $group->successTime = $this->latestReplayTime($group->successTime, $fired->recorded_at); + ParallelChildGroup::shouldWakeParentOnTimerClosure($run, $descriptor['group_path'], TimerStatus::Fired); + return true; + } + $timerTask = $this->scheduleTimer($run, $task, $itemSequence, $call, $parallelMetadata, false); + $group->scheduledTasks[] = $timerTask; + $group->pending = true; + return true; + } - if ($current instanceof ServiceOperationCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); - - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } - - if (! $this->ensureStepHistoryCompatible( - $run, - $task, - $sequence, - WorkflowStepHistory::SERVICE_OPERATION, - [ - 'operation_name' => $current->operationName, - ], - )) { - return null; - } - - $serviceEvent = $this->serviceOperationEvent($run, $sequence); - - if (! self::serviceOperationResumesOnAdmission($current)) { - $cancellationDelivery = $this->deliverCancellationAtCall( - $run, - $task, - $sequence, - 'service_operation', - $serviceEvent?->event_type === HistoryEventType::ServiceCallStarted ? null : $serviceEvent, - $workflowExecution - ); - if ($cancellationDelivery === CancellationDeliveryState::Waiting) { - return $this->waitForNextResumeSource($run, $task); - } - if ($cancellationDelivery === CancellationDeliveryState::Delivered) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->throw( - new WorkflowCancellationRequestedException( - cancellation: CooperativeCancellationDelivery::context($run), - ), - $run->cancellation_delivered_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - return null; - } - ++$sequence; - continue; - } - } - - if ($serviceEvent === null) { - try { - $serviceEvent = $this->recordServiceOperationEvent($run, $task, $sequence, $current); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - } - - if ( - $serviceEvent->event_type === HistoryEventType::ServiceCallStarted - && ! self::serviceOperationStartedEventIsVisible($serviceEvent, $current) - ) { - $this->syncWorkflowCursor($workflow, $sequence + 1); - - return $this->waitForNextResumeSource($run, $task); - } - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - - if (in_array($serviceEvent->event_type, [ - HistoryEventType::ServiceCallFailed, - HistoryEventType::ServiceCallCancelled, - ], true)) { - $current = $workflowExecution->throw( - $this->serviceOperationException($serviceEvent), - $serviceEvent->recorded_at, - ); - - $this->recordFailureHandled($run, $task, null, $sequence, $serviceEvent->payload); - } else { - $current = $workflowExecution->send( - $this->serviceOperationResult($serviceEvent, $run), - $serviceEvent->recorded_at, - ); - } - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; + private function dispatchParallelCondition( + WorkflowRun $run, + WorkflowTask $task, + array $descriptor, + WorkflowParallelDispatch $group + ): bool { + $call = $descriptor['call']; + $offset = $descriptor['offset']; + $itemSequence = $group->baseSequence + $offset; + $parallelMetadata = ParallelChildGroup::payloadForPath($descriptor['group_path']); + try { + ConditionWaits::assertReplayCompatible($run, $itemSequence, $call); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return false; + } + $resolutionEvent = $this->conditionWaitResolutionEvent($run, $itemSequence); + if ($resolutionEvent !== null) { + $group->results[$offset] = $resolutionEvent->event_type === HistoryEventType::ConditionWaitSatisfied; + $group->successTime = $this->latestReplayTime($group->successTime, $resolutionEvent->recorded_at); + ParallelChildGroup::claimSelectionWinner($run, $descriptor['group_path'], 'condition', $resolutionEvent); + return true; + } + $waitId = $this->conditionWaitId($run, $itemSequence) ?? (string) Str::ulid(); + $this->recordConditionWaitOpened( + $run, + $task, + $itemSequence, + $waitId, + $call instanceof AwaitWithTimeoutCall ? $call->seconds : null, + $call->conditionKey, + $call->conditionDefinitionFingerprint, + $parallelMetadata + ); + /** @var WorkflowTimer|null $timeoutTimer */ + $timeoutTimer = $call instanceof AwaitWithTimeoutCall ? $run->timers->firstWhere( + 'sequence', + $itemSequence + ) : null; + if (($call->condition)() === true) { + if ($timeoutTimer instanceof WorkflowTimer) { + $this->cancelConditionTimeout($run, $task, $timeoutTimer); } - - if ($current instanceof ChildWorkflowCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); - - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } - - if (! $this->ensureStepHistoryCompatible( - $run, - $task, - $sequence, - WorkflowStepHistory::CHILD_WORKFLOW, - [ - 'child_workflow_type' => $current->workflow, - ], - )) { - return null; - } - - $resolutionEvent = ChildRunHistory::resolutionEventForSequence($run, $sequence); - $childRun = ChildRunHistory::childRunForSequence($run, $sequence); - - $cancellationDelivery = $this->deliverCancellationAtCall( + $resolutionEvent = $this->recordConditionWaitSatisfied( + $run, + $task, + $itemSequence, + $waitId, + $timeoutTimer, + $call, + $parallelMetadata + ); + $group->results[$offset] = true; + $group->successTime = $this->latestReplayTime($group->successTime, $resolutionEvent->recorded_at); + ParallelChildGroup::claimSelectionWinner($run, $descriptor['group_path'], 'condition', $resolutionEvent); + return true; + } + $timeoutFired = $call instanceof AwaitWithTimeoutCall ? $this->conditionTimeoutFiredEvent( + $run, + $itemSequence + ) : null; + if ($call instanceof AwaitWithTimeoutCall && ($timeoutFired !== null || $timeoutTimer?->status === TimerStatus::Fired)) { + $resolutionEvent = $this->recordConditionWaitTimedOut( + $run, + $task, + $itemSequence, + $waitId, + $timeoutTimer, + $call->seconds, + $call->conditionKey, + $call->conditionDefinitionFingerprint, + $this->stringValue($timeoutFired?->payload['timer_id'] ?? null), + $parallelMetadata + ); + $group->results[$offset] = false; + $group->successTime = $this->latestReplayTime($group->successTime, $resolutionEvent->recorded_at); + ParallelChildGroup::claimSelectionWinner($run, $descriptor['group_path'], 'condition', $resolutionEvent); + return true; + } + if ($call instanceof AwaitWithTimeoutCall && $timeoutTimer === null) { + if ($call->seconds === 0) { + $timeoutTimer = $this->fireImmediateConditionTimeout( $run, $task, - $sequence, - 'child', - $resolutionEvent, - $workflowExecution + $itemSequence, + $waitId, + $call, + $parallelMetadata ); - if ($cancellationDelivery === CancellationDeliveryState::Waiting) { - return $this->waitForNextResumeSource($run, $task); - } - if ($cancellationDelivery === CancellationDeliveryState::Delivered) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - $current = $workflowExecution->throw( - new WorkflowCancellationRequestedException( - cancellation: CooperativeCancellationDelivery::context($run), - ), - $run->cancellation_delivered_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - if ($resolutionEvent !== null) { - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - if ($resolutionEvent->event_type === HistoryEventType::ChildRunCompleted) { - $current = $workflowExecution->send( - ChildRunHistory::outputForResolution($resolutionEvent, $childRun), - $resolutionEvent->recorded_at, - ); - } else { - $failureId = $resolutionEvent->payload['failure_id'] ?? null; - $current = $workflowExecution->throw( - ChildRunHistory::exceptionForResolution($resolutionEvent, $childRun), - $resolutionEvent->recorded_at, - ); - - $this->recordFailureHandled( - $run, - $task, - is_string($failureId) ? $failureId : null, - $sequence, - $this->childFailureHandledPayload($resolutionEvent), - ); - } - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - if ($childRun === null) { - if (ChildRunHistory::scheduledEventForSequence($run, $sequence) !== null - || ChildRunHistory::startedEventForSequence($run, $sequence) !== null) { - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->waitForNextResumeSource($run, $task); - } - - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->scheduleChildWorkflowOrFailRun($run, $task, $sequence, $current); - } - - $childStatus = ChildRunHistory::resolvedStatus(null, $childRun); - - if (in_array($childStatus, [RunStatus::Pending, RunStatus::Running, RunStatus::Waiting], true)) { - if (! $this->ensureTypedStepHistoryRecorded( - $run, - $task, - $sequence, - WorkflowStepHistory::CHILD_WORKFLOW - )) { - return null; - } - - $this->syncWorkflowCursor($workflow, $sequence + 1); - return $this->waitForNextResumeSource($run, $task); - } - - if (! $this->ensureTypedStepHistoryRecorded( + $resolutionEvent = $this->recordConditionWaitTimedOut( $run, $task, - $sequence, - WorkflowStepHistory::CHILD_WORKFLOW - )) { - return null; - } - - $resolutionEvent = $this->recordChildResolution($run, $task, $sequence, $childRun); - - try { - $this->syncWorkflowCursor($workflow, $sequence + 1); - if ($resolutionEvent->event_type === HistoryEventType::ChildRunCompleted) { - $current = $workflowExecution->send( - ChildRunHistory::outputForResolution($resolutionEvent, $childRun), - $resolutionEvent->recorded_at, - ); - } else { - $failureId = $resolutionEvent->payload['failure_id'] ?? null; - $current = $workflowExecution->throw( - ChildRunHistory::exceptionForResolution($resolutionEvent, $childRun), - $resolutionEvent->recorded_at, - ); - - $this->recordFailureHandled( - $run, - $task, - is_string($failureId) ? $failureId : null, - $sequence, - $this->childFailureHandledPayload($resolutionEvent), - ); - } - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - ++$sequence; - continue; - } - - if ($current instanceof AllCall) { - $this->syncWorkflowCursorForCurrent($workflow, $run, $current, $sequence); - - if (! $this->applyRecordedUpdates($run, $workflow, $sequence, $task)) { - return $this->restartAfterPendingUpdateFailure($run, $task); - } - - $leafDescriptors = $current->leafDescriptors($sequence); - $groupSize = count($leafDescriptors); - - try { - StructuralLimits::guardCommandBatchSize($groupSize); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - if ($groupSize === 0) { - try { - $this->syncWorkflowCursor($workflow, $sequence); - $current = $workflowExecution->send($current->nestedResults([])); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - continue; - } - - foreach ($leafDescriptors as $descriptor) { - $call = $descriptor['call']; - $itemSequence = $sequence + $descriptor['offset']; - - if (! $this->ensureStepHistoryCompatible( - $run, - $task, - $itemSequence, - match (true) { - $call instanceof ActivityCall => WorkflowStepHistory::ACTIVITY, - $call instanceof TimerCall => WorkflowStepHistory::TIMER, - $call instanceof SignalCall => WorkflowStepHistory::SIGNAL_WAIT, - $call instanceof AwaitCall, - $call instanceof AwaitWithTimeoutCall => WorkflowStepHistory::CONDITION_WAIT, - default => WorkflowStepHistory::CHILD_WORKFLOW, - }, - $call instanceof ActivityCall - ? [ - 'activity_type' => $call->activity, - ] - : ($call instanceof ChildWorkflowCall ? [ - 'child_workflow_type' => $call->workflow, - ] : []), - )) { - return null; - } - } - - if (! $this->ensureParallelGroupHistoryCompatible($run, $task, $sequence, $leafDescriptors)) { - return null; - } - - $groupResolution = $this->parallelResolutionEvent($run, $current, $sequence, $leafDescriptors); - - $cancellationDelivery = $this->deliverCancellationAtCall( + $itemSequence, + $waitId, + $timeoutTimer, + $call->seconds, + $call->conditionKey, + $call->conditionDefinitionFingerprint, + $timeoutTimer->id, + $parallelMetadata + ); + $group->results[$offset] = false; + $group->successTime = $this->latestReplayTime($group->successTime, $resolutionEvent->recorded_at); + ParallelChildGroup::claimSelectionWinner( $run, - $task, - $sequence, - 'parallel', - $groupResolution, - $workflowExecution, - $current + $descriptor['group_path'], + 'condition', + $resolutionEvent ); - if ($cancellationDelivery === CancellationDeliveryState::Waiting) { - return $this->waitForNextResumeSource($run, $task); - } - if ($cancellationDelivery === CancellationDeliveryState::Delivered) { - try { - $this->syncWorkflowCursor($workflow, $sequence + $groupSize); - $current = $workflowExecution->throw( - new WorkflowCancellationRequestedException( - cancellation: CooperativeCancellationDelivery::context($run), - ), - $run->cancellation_delivered_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - $sequence += $groupSize; - continue; - } - - $scheduledTasks = []; - $pending = false; - $results = []; - $failure = null; - $failures = []; - $successTime = null; - - foreach ($leafDescriptors as $descriptor) { - $call = $descriptor['call']; - $offset = $descriptor['offset']; - $itemSequence = $sequence + $offset; - $parallelMetadata = ParallelChildGroup::payloadForPath($descriptor['group_path']); - - if ($call instanceof ActivityCall) { - $activityCompletion = $this->activityCompletionEvent($run, $itemSequence); - - if ($activityCompletion !== null) { - if ($activityCompletion->event_type === HistoryEventType::ActivityCompleted) { - $results[$offset] = $this->activityResult($activityCompletion, $run); - $successTime = $this->latestReplayTime( - $successTime, - $activityCompletion->recorded_at ?? $activityCompletion->created_at, - ); - - continue; - } - - $failure = ParallelFailureSelector::select( - $failure, - $offset, - $this->activityException($activityCompletion, null, $run), - $activityCompletion->recorded_at?->getTimestampMs() - ?? $activityCompletion->created_at?->getTimestampMs() - ?? PHP_INT_MAX, - is_string($activityCompletion->payload['failure_id'] ?? null) - ? $activityCompletion->payload['failure_id'] - : null, - $activityCompletion->payload, - ); - $failures[$offset] = $this->activityException($activityCompletion, null, $run); - - continue; - } - - if ($this->activityOpenEvent($run, $itemSequence) !== null) { - $pending = true; - - continue; - } - - /** @var ActivityExecution|null $execution */ - $execution = $run->activityExecutions->firstWhere('sequence', $itemSequence); - - if (! $execution instanceof ActivityExecution) { - $activityTask = $this->scheduleActivityOrFailRun( - $run, - $task, - $itemSequence, - $call, - $parallelMetadata, - false, - ); - if (! $activityTask instanceof WorkflowTask) { - return null; - } - - $scheduledTasks[] = $activityTask; - $pending = true; - - continue; - } - - if (in_array($execution->status, [ - ActivityStatus::Pending, - ActivityStatus::Running, - ], true)) { - if (! $this->ensureTypedStepHistoryRecorded( - $run, - $task, - $itemSequence, - WorkflowStepHistory::ACTIVITY, - )) { - return null; - } - - $pending = true; - - continue; - } - - if (! $this->ensureTypedStepHistoryRecorded( - $run, - $task, - $itemSequence, - WorkflowStepHistory::ACTIVITY, - )) { - return null; - } - - if ($execution->status === ActivityStatus::Completed) { - $results[$offset] = $execution->activityResult(); - $successTime = $this->latestReplayTime( - $successTime, - $execution->closed_at ?? $execution->updated_at ?? $execution->created_at, - ); - - continue; - } - - /** @var WorkflowFailure|null $activityFailure */ - $activityFailure = $run->failures->firstWhere('source_id', $execution->id); - - $failure = ParallelFailureSelector::select( - $failure, - $offset, - $this->activityException(null, $execution, $run), - $execution->closed_at?->getTimestampMs() ?? PHP_INT_MAX, - $activityFailure?->id, - [ - 'source_kind' => 'activity_execution', - 'source_id' => $execution->id, - 'exception_class' => $activityFailure?->exception_class, - 'message' => $activityFailure?->message, - ], - ); - $failures[$offset] = $this->activityException(null, $execution, $run); - - continue; - } - - if ($call instanceof TimerCall) { - $timerFired = $this->timerFiredEvent($run, $itemSequence); - - if ($timerFired !== null) { - $results[$offset] = true; - $successTime = $this->latestReplayTime( - $successTime, - $timerFired->recorded_at ?? $timerFired->created_at, - ); - - continue; - } - - if ($this->timerScheduledEvent($run, $itemSequence) !== null) { - $pending = true; - - continue; - } - - /** @var WorkflowTimer|null $timer */ - $timer = $run->timers->firstWhere('sequence', $itemSequence); - - if ($timer instanceof WorkflowTimer) { - if ($timer->status === TimerStatus::Fired) { - $results[$offset] = true; - $successTime = $this->latestReplayTime( - $successTime, - $timer->fired_at ?? $timer->updated_at ?? $timer->created_at, - ); - } else { - $pending = true; - } - - continue; - } - - if ($call->seconds === 0) { - $fired = $this->fireImmediateTimer( - $run, - $task, - $itemSequence, - $call, - $parallelMetadata, - ); - $results[$offset] = true; - $successTime = $this->latestReplayTime($successTime, $fired->recorded_at); - ParallelChildGroup::shouldWakeParentOnTimerClosure( - $run, - $descriptor['group_path'], - TimerStatus::Fired, - ); - - continue; - } - - $timerTask = $this->scheduleTimer( - $run, - $task, - $itemSequence, - $call, - $parallelMetadata, - false, - ); - $scheduledTasks[] = $timerTask; - $pending = true; - - continue; - } - - if ($call instanceof AwaitCall || $call instanceof AwaitWithTimeoutCall) { - try { - ConditionWaits::assertReplayCompatible($run, $itemSequence, $call); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - $resolutionEvent = $this->conditionWaitResolutionEvent($run, $itemSequence); - - if ($resolutionEvent !== null) { - $results[$offset] = $resolutionEvent->event_type === HistoryEventType::ConditionWaitSatisfied; - $successTime = $this->latestReplayTime($successTime, $resolutionEvent->recorded_at); - ParallelChildGroup::claimSelectionWinner( - $run, - $descriptor['group_path'], - 'condition', - $resolutionEvent, - ); - - continue; - } - - $waitId = $this->conditionWaitId($run, $itemSequence) ?? (string) Str::ulid(); - $this->recordConditionWaitOpened( - $run, - $task, - $itemSequence, - $waitId, - $call instanceof AwaitWithTimeoutCall ? $call->seconds : null, - $call->conditionKey, - $call->conditionDefinitionFingerprint, - $parallelMetadata, - ); - /** @var WorkflowTimer|null $timeoutTimer */ - $timeoutTimer = $call instanceof AwaitWithTimeoutCall - ? $run->timers->firstWhere('sequence', $itemSequence) - : null; - - if (($call->condition)() === true) { - if ($timeoutTimer instanceof WorkflowTimer) { - $this->cancelConditionTimeout($run, $task, $timeoutTimer); - } - - $resolutionEvent = $this->recordConditionWaitSatisfied( - $run, - $task, - $itemSequence, - $waitId, - $timeoutTimer, - $call, - $parallelMetadata, - ); - $results[$offset] = true; - $successTime = $this->latestReplayTime($successTime, $resolutionEvent->recorded_at); - ParallelChildGroup::claimSelectionWinner( - $run, - $descriptor['group_path'], - 'condition', - $resolutionEvent, - ); - - continue; - } - - $timeoutFired = $call instanceof AwaitWithTimeoutCall - ? $this->conditionTimeoutFiredEvent($run, $itemSequence) - : null; - if ($call instanceof AwaitWithTimeoutCall && ( - $timeoutFired !== null || $timeoutTimer?->status === TimerStatus::Fired - )) { - $resolutionEvent = $this->recordConditionWaitTimedOut( - $run, - $task, - $itemSequence, - $waitId, - $timeoutTimer, - $call->seconds, - $call->conditionKey, - $call->conditionDefinitionFingerprint, - $this->stringValue($timeoutFired?->payload['timer_id'] ?? null), - $parallelMetadata, - ); - $results[$offset] = false; - $successTime = $this->latestReplayTime($successTime, $resolutionEvent->recorded_at); - ParallelChildGroup::claimSelectionWinner( - $run, - $descriptor['group_path'], - 'condition', - $resolutionEvent, - ); - - continue; - } - - if ($call instanceof AwaitWithTimeoutCall && $timeoutTimer === null) { - if ($call->seconds === 0) { - $timeoutTimer = $this->fireImmediateConditionTimeout( - $run, - $task, - $itemSequence, - $waitId, - $call, - $parallelMetadata, - ); - - $resolutionEvent = $this->recordConditionWaitTimedOut( - $run, - $task, - $itemSequence, - $waitId, - $timeoutTimer, - $call->seconds, - $call->conditionKey, - $call->conditionDefinitionFingerprint, - $timeoutTimer->id, - $parallelMetadata, - ); - $results[$offset] = false; - $successTime = $this->latestReplayTime( - $successTime, - $resolutionEvent->recorded_at, - ); - ParallelChildGroup::claimSelectionWinner( - $run, - $descriptor['group_path'], - 'condition', - $resolutionEvent, - ); - - continue; - } - $scheduledTasks[] = $this->scheduleConditionTimeout( - $run, - $task, - $itemSequence, - $waitId, - $call, - false, - ); - } - - $pending = true; - - continue; - } - - if ($call instanceof SignalCall) { - $signalEvent = $this->appliedSignalEvent($run, $itemSequence, $call); - - if ($signalEvent !== null) { - $results[$offset] = $this->signalValue($signalEvent, $run); - $successTime = $this->latestReplayTime($successTime, $signalEvent->recorded_at); - ParallelChildGroup::claimSelectionWinner( - $run, - $descriptor['group_path'], - 'signal', - $signalEvent, - ); - - continue; - } - - /** @var WorkflowTimer|null $timeoutTimer */ - $timeoutTimer = $call->timeoutSeconds !== null - ? $run->timers->firstWhere('sequence', $itemSequence) - : null; - $timeoutScheduled = $call->timeoutSeconds !== null - ? $this->signalTimeoutScheduledEvent($run, $itemSequence, $call->name) - : null; - $timeoutFired = $call->timeoutSeconds !== null - ? $this->signalTimeoutFiredEvent($run, $itemSequence, $call->name) - : null; - $signalWaitId = $this->signalWaitId($run, $itemSequence, $call) ?? (string) Str::ulid(); - $signalCommand = $this->pendingSignalCommand($run, $call); - - $timeoutHasWon = $call->timeoutSeconds !== null && ( - $timeoutFired !== null - || ($timeoutScheduled === null && $timeoutTimer?->status === TimerStatus::Fired) - ); - if ($timeoutHasWon && $signalCommand !== null && $timeoutFired !== null) { - $signalReceived = $this->signalReceivedEventForCommand($run, $signalCommand); - if ($signalReceived !== null && $signalReceived->sequence < $timeoutFired->sequence) { - $timeoutHasWon = false; - } - } - - if ($timeoutHasWon) { - $this->recordSignalWait( - $run, - $task, - $itemSequence, - $call, - $signalWaitId, - $parallelMetadata, - ); - $resolutionEvent = $timeoutFired ?? $this->signalTimeoutFiredEvent( - $run, - $itemSequence, - $call->name, - ); - if ($resolutionEvent instanceof WorkflowHistoryEvent) { - $results[$offset] = null; - $successTime = $this->latestReplayTime( - $successTime, - $resolutionEvent->recorded_at, - ); - ParallelChildGroup::claimSelectionWinner( - $run, - $descriptor['group_path'], - 'signal', - $resolutionEvent, - ); - - continue; - } - } - - if ($signalCommand !== null) { - $signalWaitId = $this->signalWaitIdForCommand($run, $signalCommand, $call->name); - $this->recordSignalWait( - $run, - $task, - $itemSequence, - $call, - $signalWaitId, - $parallelMetadata, - ); - if ($timeoutTimer instanceof WorkflowTimer) { - $this->cancelSignalTimeout($run, $task, $timeoutTimer); - } - $signalEvent = $this->applySignal( - $run, - $task, - $itemSequence, - $call, - $signalCommand, - $signalWaitId, - $parallelMetadata, - ); - $results[$offset] = $this->signalValue($signalEvent, $run); - $successTime = $this->latestReplayTime($successTime, $signalEvent->recorded_at); - ParallelChildGroup::claimSelectionWinner( - $run, - $descriptor['group_path'], - 'signal', - $signalEvent, - ); - - continue; - } - - $this->recordSignalWait( - $run, - $task, - $itemSequence, - $call, - $signalWaitId, - $parallelMetadata, - ); - - if ($call->timeoutSeconds !== null && $timeoutTimer === null) { - if ($call->timeoutSeconds === 0) { - $timeoutTimer = $this->fireImmediateSignalTimeout( - $run, - $task, - $itemSequence, - $signalWaitId, - $call, - $parallelMetadata, - ); - $run->unsetRelation('historyEvents'); - $run->load('historyEvents'); - $resolutionEvent = $this->signalTimeoutFiredEvent( - $run, - $itemSequence, - $call->name, - ); - if ($resolutionEvent instanceof WorkflowHistoryEvent) { - $results[$offset] = null; - $successTime = $this->latestReplayTime( - $successTime, - $resolutionEvent->recorded_at, - ); - ParallelChildGroup::claimSelectionWinner( - $run, - $descriptor['group_path'], - 'signal', - $resolutionEvent, - ); - - continue; - } - } else { - $scheduledTasks[] = $this->scheduleSignalTimeout( - $run, - $task, - $itemSequence, - $signalWaitId, - $call, - false, - ); - } - } - - $pending = true; - - continue; - } - - $resolutionEvent = ChildRunHistory::resolutionEventForSequence($run, $itemSequence); - $childRun = ChildRunHistory::childRunForSequence($run, $itemSequence); - - if ($resolutionEvent === null && $childRun instanceof WorkflowRun) { - $childStatus = ChildRunHistory::resolvedStatus(null, $childRun); - - if ($childStatus instanceof RunStatus && ! in_array($childStatus, [ - RunStatus::Pending, - RunStatus::Running, - RunStatus::Waiting, - ], true)) { - if (! $this->ensureTypedStepHistoryRecorded( - $run, - $task, - $itemSequence, - WorkflowStepHistory::CHILD_WORKFLOW, - )) { - return null; - } - - $resolutionEvent = $this->recordChildResolution($run, $task, $itemSequence, $childRun); - } - } - - if ($resolutionEvent !== null) { - if ($resolutionEvent->event_type === HistoryEventType::ChildRunCompleted) { - $results[$offset] = ChildRunHistory::outputForResolution($resolutionEvent, $childRun); - $successTime = $this->latestReplayTime( - $successTime, - $resolutionEvent->recorded_at ?? $resolutionEvent->created_at, - ); - - continue; - } - - $failure = ParallelFailureSelector::select( - $failure, - $offset, - ChildRunHistory::exceptionForResolution($resolutionEvent, $childRun), - $resolutionEvent->recorded_at?->getTimestampMs() - ?? $resolutionEvent->created_at?->getTimestampMs() - ?? PHP_INT_MAX, - is_string($resolutionEvent->payload['failure_id'] ?? null) - ? $resolutionEvent->payload['failure_id'] - : null, - $this->childFailureHandledPayload($resolutionEvent), - ); - $failures[$offset] = ChildRunHistory::exceptionForResolution($resolutionEvent, $childRun); - - continue; - } - - if (! $childRun instanceof WorkflowRun) { - if ( - ChildRunHistory::scheduledEventForSequence($run, $itemSequence) !== null - || ChildRunHistory::startedEventForSequence($run, $itemSequence) !== null - ) { - $pending = true; - - continue; - } - - $childTask = $this->scheduleChildWorkflowOrFailRun( - $run, - $task, - $itemSequence, - $call, - $parallelMetadata, - false, - ); - if (! $childTask instanceof WorkflowTask) { - return null; - } - - $scheduledTasks[] = $childTask; - $pending = true; - - continue; - } - - $childStatus = ChildRunHistory::resolvedStatus(null, $childRun); - - if (! $childStatus instanceof RunStatus || in_array($childStatus, [ - RunStatus::Pending, - RunStatus::Running, - RunStatus::Waiting, - ], true)) { - if (! $this->ensureTypedStepHistoryRecorded( - $run, - $task, - $itemSequence, - WorkflowStepHistory::CHILD_WORKFLOW, - )) { - return null; - } - - $pending = true; - - continue; - } - - if (! $this->ensureTypedStepHistoryRecorded( - $run, - $task, - $itemSequence, - WorkflowStepHistory::CHILD_WORKFLOW, - )) { - return null; - } - - if ($childStatus === RunStatus::Completed) { - $results[$offset] = ChildRunHistory::outputForChildRun($childRun); - $successTime = $this->latestReplayTime( - $successTime, - $childRun->closed_at ?? $childRun->updated_at ?? $childRun->created_at, - ); + return true; + } + $group->scheduledTasks[] = $this->scheduleConditionTimeout( + $run, + $task, + $itemSequence, + $waitId, + $call, + false + ); + } + $group->pending = true; + return true; + } - continue; - } + private function dispatchParallelSignal( + WorkflowRun $run, + WorkflowTask $task, + array $descriptor, + WorkflowParallelDispatch $group + ): bool { + $call = $descriptor['call']; + $offset = $descriptor['offset']; + $itemSequence = $group->baseSequence + $offset; + $parallelMetadata = ParallelChildGroup::payloadForPath($descriptor['group_path']); + $signalEvent = $this->appliedSignalEvent($run, $itemSequence, $call); + if ($signalEvent !== null) { + $group->results[$offset] = $this->signalValue($signalEvent, $run); + $group->successTime = $this->latestReplayTime($group->successTime, $signalEvent->recorded_at); + ParallelChildGroup::claimSelectionWinner($run, $descriptor['group_path'], 'signal', $signalEvent); + return true; + } + /** @var WorkflowTimer|null $timeoutTimer */ + $timeoutTimer = $call->timeoutSeconds !== null ? $run->timers->firstWhere('sequence', $itemSequence) : null; + $timeoutScheduled = $call->timeoutSeconds !== null ? $this->signalTimeoutScheduledEvent( + $run, + $itemSequence, + $call->name + ) : null; + $timeoutFired = $call->timeoutSeconds !== null ? $this->signalTimeoutFiredEvent( + $run, + $itemSequence, + $call->name + ) : null; + $signalWaitId = $this->signalWaitId($run, $itemSequence, $call) ?? (string) Str::ulid(); + $signalCommand = $this->pendingSignalCommand($run, $call); + $timeoutHasWon = $this->signalTimeoutHasWon( + $run, + $call->timeoutSeconds !== null, + $timeoutTimer, + $timeoutScheduled, + $timeoutFired, + $signalCommand, + ); + if ($timeoutHasWon) { + $this->recordSignalWait($run, $task, $itemSequence, $call, $signalWaitId, $parallelMetadata); + $resolutionEvent = $timeoutFired ?? $this->signalTimeoutFiredEvent($run, $itemSequence, $call->name); + if ($resolutionEvent instanceof WorkflowHistoryEvent) { + $group->results[$offset] = null; + $group->successTime = $this->latestReplayTime($group->successTime, $resolutionEvent->recorded_at); + ParallelChildGroup::claimSelectionWinner($run, $descriptor['group_path'], 'signal', $resolutionEvent); + return true; + } + } + if ($signalCommand !== null) { + $signalWaitId = $this->signalWaitIdForCommand($run, $signalCommand, $call->name); + $this->recordSignalWait($run, $task, $itemSequence, $call, $signalWaitId, $parallelMetadata); + if ($timeoutTimer instanceof WorkflowTimer) { + $this->cancelSignalTimeout($run, $task, $timeoutTimer); + } + $signalEvent = $this->applySignal( + $run, + $task, + $itemSequence, + $call, + $signalCommand, + $signalWaitId, + $parallelMetadata + ); + $group->results[$offset] = $this->signalValue($signalEvent, $run); + $group->successTime = $this->latestReplayTime($group->successTime, $signalEvent->recorded_at); + ParallelChildGroup::claimSelectionWinner($run, $descriptor['group_path'], 'signal', $signalEvent); + return true; + } + return $this->dispatchParallelSignalTimeout( + $run, + $task, + $descriptor, + $group, + $signalWaitId, + $timeoutTimer, + $parallelMetadata + ); + } - $failure = ParallelFailureSelector::select( - $failure, - $offset, - ChildRunHistory::exceptionForChildRun($childRun), - $childRun->closed_at?->getTimestampMs() ?? PHP_INT_MAX, + private function dispatchParallelSignalTimeout( + WorkflowRun $run, + WorkflowTask $task, + array $descriptor, + WorkflowParallelDispatch $group, + string $signalWaitId, + ?WorkflowTimer $timeoutTimer, + array $parallelMetadata, + ): bool { + $call = $descriptor['call']; + $offset = $descriptor['offset']; + $itemSequence = $group->baseSequence + $offset; + $this->recordSignalWait($run, $task, $itemSequence, $call, $signalWaitId, $parallelMetadata); + if ($call->timeoutSeconds !== null && $timeoutTimer === null) { + if ($call->timeoutSeconds === 0) { + $this->fireImmediateSignalTimeout($run, $task, $itemSequence, $signalWaitId, $call, $parallelMetadata); + $run->unsetRelation('historyEvents'); + $run->load('historyEvents'); + $resolutionEvent = $this->signalTimeoutFiredEvent($run, $itemSequence, $call->name); + if ($resolutionEvent instanceof WorkflowHistoryEvent) { + $group->results[$offset] = null; + $group->successTime = $this->latestReplayTime($group->successTime, $resolutionEvent->recorded_at); + ParallelChildGroup::claimSelectionWinner( + $run, + $descriptor['group_path'], + 'signal', + $resolutionEvent ); - $failures[$offset] = ChildRunHistory::exceptionForChildRun($childRun); - } - - if ($scheduledTasks !== []) { - $this->logApproachingLimit(StructuralLimits::warnApproachingCommandBatch($groupSize), $run); - } - - if ($current instanceof SelectCall) { - $groupId = ParallelChildGroup::payloadForPath($leafDescriptors[0]['group_path'])[ - 'parallel_group_path' - ][0]['parallel_group_id']; - $selection = ParallelChildGroup::selectionResolution($run, $groupId); - - if ($selection instanceof WorkflowHistoryEvent) { - try { - $winner = ParallelChildGroup::validatedSelectionResolution( - $run, - $current, - $sequence, - $selection, - ); - $this->syncWorkflowCursor($workflow, $sequence + $groupSize); - $current = $workflowExecution->send( - $current->resolved( - $sequence, - $winner, - $results, - $failures, - ParallelChildGroup::durableOperationIdentities($run), - ), - $selection->recorded_at, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - $sequence += $groupSize; - - continue; - } - } elseif ($failure !== null) { - try { - $this->syncWorkflowCursor($workflow, $sequence + $groupSize); - $failureTime = isset($failure['recorded_at']) && is_int( - $failure['recorded_at'] - ) && $failure['recorded_at'] !== PHP_INT_MAX - ? \Carbon\Carbon::createFromTimestampMs($failure['recorded_at']) - : null; - $current = $workflowExecution->throw($failure['exception'], $failureTime); - - $this->recordFailureHandled( - $run, - $task, - is_string($failure['failure_id'] ?? null) ? $failure['failure_id'] : null, - $sequence + (int) $failure['index'], - is_array($failure['failure_payload'] ?? null) ? $failure['failure_payload'] : [], - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - $sequence += $groupSize; - - continue; - } - - if (! $pending) { - ksort($results); - - try { - $this->syncWorkflowCursor($workflow, $sequence + $groupSize); - $current = $workflowExecution->send( - $current->nestedResults(array_values($results)), - $successTime, - ); - } catch (Throwable $throwable) { - $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); - - return null; - } - - $sequence += $groupSize; - - continue; - } - - $this->markRunWaiting($run, $task); - - foreach ($scheduledTasks as $scheduledTask) { - TaskDispatcher::dispatch($scheduledTask); + return true; } - - $this->syncWorkflowCursor($workflow, $sequence + $groupSize); - return null; + } else { + $group->scheduledTasks[] = $this->scheduleSignalTimeout( + $run, + $task, + $itemSequence, + $signalWaitId, + $call, + false + ); } + } + $group->pending = true; + return true; + } - if ($current instanceof ContinueAsNewCall) { - if (! $this->ensureStepHistoryCompatible( + private function dispatchParallelChild( + WorkflowRun $run, + WorkflowTask $task, + array $descriptor, + WorkflowParallelDispatch $group + ): bool { + $offset = $descriptor['offset']; + $itemSequence = $group->baseSequence + $offset; + $parallelMetadata = ParallelChildGroup::payloadForPath($descriptor['group_path']); + $resolutionEvent = ChildRunHistory::resolutionEventForSequence($run, $itemSequence); + $childRun = ChildRunHistory::childRunForSequence($run, $itemSequence); + if ($resolutionEvent === null && $childRun instanceof WorkflowRun) { + $childStatus = ChildRunHistory::resolvedStatus(null, $childRun); + if ($childStatus instanceof RunStatus && ! in_array( + $childStatus, + [RunStatus::Pending, RunStatus::Running, RunStatus::Waiting], + true + )) { + if (! $this->ensureTypedStepHistoryRecorded( $run, $task, - $sequence, - WorkflowStepHistory::CONTINUE_AS_NEW + $itemSequence, + WorkflowStepHistory::CHILD_WORKFLOW )) { - return null; - } - - try { - return $this->continueAsNew($run, $task, $sequence, $current, $workflowClass); - } catch (StructuralLimitExceededException $limitExceeded) { - $this->failRun($run, $task, $limitExceeded, 'workflow_run', $run->id); - - return null; + return false; } + $resolutionEvent = $this->recordChildResolution($run, $task, $itemSequence, $childRun); } - - $this->failRun( + } + if ($resolutionEvent !== null) { + return $this->resolveParallelChildFromEvent($childRun, $resolutionEvent, $offset, $group); + } + if (! $childRun instanceof WorkflowRun) { + return $this->dispatchMissingParallelChild( $run, $task, - new UnsupportedWorkflowYieldException(sprintf( - 'Workflow %s yielded %s. v2 currently supports activity(), child(), async(), all(), parallel(), await(), signal(), timer(), sideEffect(), continueAsNew(), getVersion(), patched(), deprecatePatch(), upsertMemo(), and upsertSearchAttributes() only.', - $run->workflow_class, - get_debug_type($current), - )), - 'workflow_run', - $run->id, + $descriptor, + $group, + $itemSequence, + $parallelMetadata ); - - return null; } - } - - /** - * Close a run whose server-enforced execution or run deadline elapsed. - * - * External worker bridges call this while holding the task and run locks, - * immediately before accepting commands. This makes the deadline the - * authority at the workflow-task commit boundary, even when the watchdog - * cannot repair a run because its task remains leased. - */ - public function timeoutIfDeadlineExpired(WorkflowRun $run, WorkflowTask $task): bool - { - $run->load([ - 'instance', - 'activityExecutions', - 'timers', - 'failures', - 'tasks', - 'commands', - 'updates', - 'historyEvents', - 'childLinks.childRun.instance.currentRun', - 'childLinks.childRun.failures', - 'childLinks.childRun.historyEvents', - ]); - - if (! $this->deadlineExpired($run)) { + $childStatus = ChildRunHistory::resolvedStatus(null, $childRun); + if (! $childStatus instanceof RunStatus || in_array( + $childStatus, + [RunStatus::Pending, RunStatus::Running, RunStatus::Waiting], + true + )) { + if (! $this->ensureTypedStepHistoryRecorded( + $run, + $task, + $itemSequence, + WorkflowStepHistory::CHILD_WORKFLOW + )) { + return false; + } + $group->pending = true; + return true; + } + if (! $this->ensureTypedStepHistoryRecorded($run, $task, $itemSequence, WorkflowStepHistory::CHILD_WORKFLOW)) { return false; } + return $this->resolveParallelChildRun($childRun, $childStatus, $offset, $group); + } - $this->timeoutRun($run, $task); - + private function resolveParallelChildRun( + WorkflowRun $childRun, + RunStatus $childStatus, + int $offset, + WorkflowParallelDispatch $group, + ): bool { + if ($childStatus === RunStatus::Completed) { + $group->results[$offset] = ChildRunHistory::outputForChildRun($childRun); + $group->successTime = $this->latestReplayTime( + $group->successTime, + $childRun->closed_at ?? $childRun->updated_at ?? $childRun->created_at + ); + return true; + } + $group->failure = ParallelFailureSelector::select( + $group->failure, + $offset, + ChildRunHistory::exceptionForChildRun($childRun), + $childRun->closed_at?->getTimestampMs() ?? PHP_INT_MAX + ); + $group->failures[$offset] = ChildRunHistory::exceptionForChildRun($childRun); return true; } - /** - * Finish portable cleanup with the original cancellation outcome. - * The caller holds the task and run locks before accepting a terminal command. - */ - public function finishPortableCooperativeCancellation(WorkflowRun $run, WorkflowTask $task): bool - { - if (! is_string($run->cancellation_request_command_id)) { - return false; + private function resolveParallelChildFromEvent( + ?WorkflowRun $childRun, + WorkflowHistoryEvent $resolutionEvent, + int $offset, + WorkflowParallelDispatch $group, + ): bool { + if ($resolutionEvent->event_type === HistoryEventType::ChildRunCompleted) { + $group->results[$offset] = ChildRunHistory::outputForResolution($resolutionEvent, $childRun); + $group->successTime = $this->latestReplayTime( + $group->successTime, + $resolutionEvent->recorded_at ?? $resolutionEvent->created_at + ); + return true; } - $run->load([ - 'instance', 'activityExecutions', 'timers', 'failures', 'tasks', 'commands', - 'updates', 'historyEvents', 'childLinks.childRun.instance.currentRun', - 'childLinks.childRun.failures', 'childLinks.childRun.historyEvents', - ]); - $this->finishCooperativeCancellation($run, $task, 'Cooperative cancellation completed.'); - + $group->failure = ParallelFailureSelector::select( + $group->failure, + $offset, + ChildRunHistory::exceptionForResolution($resolutionEvent, $childRun), + $resolutionEvent->recorded_at?->getTimestampMs() ?? $resolutionEvent->created_at?->getTimestampMs() ?? PHP_INT_MAX, + is_string($resolutionEvent->payload['failure_id'] ?? null) ? $resolutionEvent->payload['failure_id'] : null, + $this->childFailureHandledPayload($resolutionEvent) + ); + $group->failures[$offset] = ChildRunHistory::exceptionForResolution($resolutionEvent, $childRun); return true; } - public function cancelIfCleanupDeadlineExpired(WorkflowRun $run, WorkflowTask $task): bool - { - if ( - $run->status->isTerminal() - || ! is_string($run->cancellation_request_command_id) - || $run->cancellation_deadline_at === null - || now() - ->lt($run->cancellation_deadline_at) - ) { + private function dispatchMissingParallelChild( + WorkflowRun $run, + WorkflowTask $task, + array $descriptor, + WorkflowParallelDispatch $group, + int $itemSequence, + array $parallelMetadata, + ): bool { + if (ChildRunHistory::scheduledEventForSequence( + $run, + $itemSequence + ) !== null || ChildRunHistory::startedEventForSequence($run, $itemSequence) !== null) { + $group->pending = true; + return true; + } + $childTask = $this->scheduleChildWorkflowOrFailRun( + $run, + $task, + $itemSequence, + $descriptor['call'], + $parallelMetadata, + false + ); + if (! $childTask instanceof WorkflowTask) { return false; } - - $run->load([ - 'instance', - 'activityExecutions', - 'timers', - 'failures', - 'tasks', - 'commands', - 'updates', - 'historyEvents', - 'childLinks.childRun.instance.currentRun', - 'childLinks.childRun.failures', - 'childLinks.childRun.historyEvents', - ]); - $this->finishCooperativeCancellation($run, $task, 'Cooperative cancellation cleanup deadline expired.'); - + $group->scheduledTasks[] = $childTask; + $group->pending = true; return true; } - /** - * The caller holds the parent run lock; child terminal history, not row status, authorizes recovery. - */ - public function recoverClosedChildResolutions(WorkflowRun $parentRun): void - { - if ($parentRun->status->isTerminal() || $parentRun->tasks() - ->where('task_type', TaskType::Workflow->value) - ->whereIn('status', [TaskStatus::Ready->value, TaskStatus::Leased->value]) - ->lockForUpdate() - ->exists()) { - return; + private function resumeCancellation( + WorkflowRun $run, + WorkflowTask $task, + WorkflowContinuation $frame, + int $size = 1, + ): ?WorkflowTaskStep { + try { + $this->syncWorkflowCursor($frame->workflow, $frame->sequence + $size); + $frame->current = $frame->execution->throw( + new WorkflowCancellationRequestedException( + cancellation: CooperativeCancellationDelivery::context($run), + ), + $run->cancellation_delivered_at, + ); + } catch (Throwable $throwable) { + $this->failRun($run, $task, $throwable, 'workflow_run', $run->id); + return null; } + $frame->sequence += $size; - $parentRun->setRelation('historyEvents', $parentRun->historyEvents()->lockForUpdate()->get()); - foreach (ChildRunHistory::knownSequences($parentRun) as $sequence) { - if (ChildRunHistory::resolutionEventForSequence($parentRun, $sequence) !== null) { - continue; - } - try { - WorkflowStepHistory::assertCompatible($parentRun, $sequence, WorkflowStepHistory::CHILD_WORKFLOW); - WorkflowStepHistory::assertTypedHistoryRecorded( - $parentRun, - $sequence, - WorkflowStepHistory::CHILD_WORKFLOW - ); - } catch (HistoryEventShapeMismatchException) { - continue; - } - - $childRun = ChildRunHistory::childRunForSequence($parentRun, $sequence); - if (! $childRun instanceof WorkflowRun || ! $childRun->historyEvents->contains( - static fn (WorkflowHistoryEvent $event): bool => in_array($event->event_type, [ - HistoryEventType::WorkflowCompleted, - HistoryEventType::WorkflowFailed, - HistoryEventType::WorkflowCancelled, - HistoryEventType::WorkflowTerminated, - ], true) - )) { - continue; - } - if ($this->startChildRetryIfAvailable($parentRun, $sequence, $childRun) === null) { - $this->recordChildResolution($parentRun, null, $sequence, $childRun); - } - $parentRun->unsetRelation('childLinks'); - $parentRun->setRelation('historyEvents', $parentRun->historyEvents()->lockForUpdate()->get()); - } + return WorkflowTaskStep::ContinueExecution; } - public function deliverPortableCancellation( + private function executeContinueAsNew( WorkflowRun $run, WorkflowTask $task, - int $sequence, - string $callKind, - int $sequenceSpan = 1, - ?int $operationSequence = null, - int $operationSequenceSpan = 1, - ): ?WorkflowHistoryEvent { - $existing = CooperativeCancellationDelivery::recorded($run); - if ($existing instanceof WorkflowHistoryEvent) { - return $existing; - } - $run->loadMissing(['tasks', 'timers', 'activityExecutions']); - $targetSequence = $operationSequence ?? $sequence; - $targetSpan = $operationSequence === null ? $sequenceSpan : $operationSequenceSpan; - $waiting = false; - for ($offset = 0; $offset < $targetSpan; ++$offset) { - $targetKind = in_array($callKind, ['parallel', 'selection_handle'], true) - ? CooperativeCancellationDelivery::callKindAt($run, $targetSequence + $offset) - : $callKind; - if ($targetKind !== null) { - $waiting = $this->cancelOpenWaitAtSequence( - $run, - $task, - $targetSequence + $offset, - $targetKind - ) || $waiting; - } - } - if ($waiting) { - ActivityCancellationWait::bindBoundary( - $task, - $sequence, - $callKind, - $sequenceSpan, - $operationSequence, - $operationSequenceSpan - ); - $this->waitForNextResumeSource($run, $task); + WorkflowContinuation $frame, + string $workflowClass, + ): WorkflowTask|WorkflowTaskStep|null { + /** @var ContinueAsNewCall $current */ + $current = &$frame->current; + $sequence = &$frame->sequence; + if (! $this->ensureStepHistoryCompatible($run, $task, $sequence, WorkflowStepHistory::CONTINUE_AS_NEW)) { + return null; + } + try { + return $this->continueAsNew($run, $task, $sequence, $current, $workflowClass); + } catch (StructuralLimitExceededException $limitExceeded) { + $this->failRun($run, $task, $limitExceeded, 'workflow_run', $run->id); return null; } - - return CooperativeCancellationDelivery::record( - $run, - $task, - $sequence, - $callKind, - $sequenceSpan, - $operationSequence, - $operationSequenceSpan, - ); } private function scheduleActivity( diff --git a/src/V2/Support/WorkflowParallelDispatch.php b/src/V2/Support/WorkflowParallelDispatch.php new file mode 100644 index 000000000..bdb4d58d5 --- /dev/null +++ b/src/V2/Support/WorkflowParallelDispatch.php @@ -0,0 +1,38 @@ + + */ + public array $scheduledTasks = []; + + public bool $pending = false; + + /** + * @var array + */ + public array $results = []; + + public ?array $failure = null; + + /** + * @var array + */ + public array $failures = []; + + public ?CarbonInterface $successTime = null; + + public function __construct( + public readonly int $baseSequence + ) { + } +} diff --git a/src/V2/Support/WorkflowTaskExecution.php b/src/V2/Support/WorkflowTaskExecution.php new file mode 100644 index 000000000..0726eec43 --- /dev/null +++ b/src/V2/Support/WorkflowTaskExecution.php @@ -0,0 +1,26 @@ + $workflowClass + */ + public function __construct( + public readonly WorkflowRun $run, + public readonly WorkflowTask $task, + public readonly WorkflowContinuation $frame, + public readonly VersionDecisions $versionDecisions, + public readonly string $workflowClass, + public readonly int $historySequenceAtTaskStart, + ) { + } +} diff --git a/src/V2/Support/WorkflowTaskStep.php b/src/V2/Support/WorkflowTaskStep.php new file mode 100644 index 000000000..09d167639 --- /dev/null +++ b/src/V2/Support/WorkflowTaskStep.php @@ -0,0 +1,11 @@ + [ + 'workflows' => [], + 'build' => null, + ], + // Sticky execution is a replay performance feature. A worker may keep // a process-local cache for a run it just executed, and matching may // prefer that worker for follow-up workflow tasks until the affinity diff --git a/tests/Feature/V2/V2EmbeddedReplayRegressionCorpusTest.php b/tests/Feature/V2/V2EmbeddedReplayRegressionCorpusTest.php index 9179202b4..a1544bfe0 100644 --- a/tests/Feature/V2/V2EmbeddedReplayRegressionCorpusTest.php +++ b/tests/Feature/V2/V2EmbeddedReplayRegressionCorpusTest.php @@ -4,10 +4,13 @@ namespace Tests\Feature\V2; +use Illuminate\Queue\WorkerOptions; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Queue; +use PHPUnit\Framework\Attributes\PreserveGlobalState; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use Symfony\Component\Uid\Ulid; use Tests\Fixtures\V2\TestQueryReplayGuardActivity; use Tests\Fixtures\V2\TestRepeatedPatchClockWorkflow; @@ -77,6 +80,97 @@ final class V2EmbeddedReplayRegressionCorpusTest extends TestCase private int $workflowNumber = 0; + #[RunInSeparateProcess] + #[PreserveGlobalState(false)] + public function testCommittedTimerContinuationUsesOneWorkflowConstruction(): void + { + $path = self::FIXTURE_DIR . '/embedded-committed-timer-continuation.json'; + if (! is_file($path)) { + $this->markTestSkipped('The retained timer evidence is not present in this source revision.'); + } + $fixture = json_decode(file_get_contents($path), true, flags: JSON_THROW_ON_ERROR); + $this->clearWorkflowState(); + $previousQueue = Queue::getFacadeRoot(); + $keys = ['queue.default', 'queue.connections.database', 'workflows.v2.queue', + 'workflows.v2.task_dispatch_mode', 'workflows.v2.live_execution']; + $previous = array_combine($keys, array_map(static fn (string $key): mixed => config($key), $keys)); + config([ + 'queue.default' => 'database', + 'queue.connections.database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'corpus-live', + 'retry_after' => 90, + ], + 'workflows.v2.queue' => 'corpus-live', + 'workflows.v2.task_dispatch_mode' => 'queue', + 'workflows.v2.live_execution' => [ + 'workflows' => [$fixture['workflow']['type']], + 'build' => 'corpus-live', + ], + ]); + $this->app->forgetInstance('queue'); + Queue::swap($this->app->make('queue')); + $constructions = 0; + $count = false; + $this->app->resolving($fixture['workflow']['type'], static function () use (&$constructions, &$count): void { + if ($count) { + ++$constructions; + } + }); + Carbon::setTestNow('2026-10-17 15:15:00.123456'); + try { + $stub = WorkflowStub::make($fixture['workflow']['type'], 'corpus-live'); + $stub->start(...[...$fixture['workflow']['arguments'], new StartOptions( + executionTimeoutSeconds: 120, + runTimeoutSeconds: 90, + )]); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')]; + $worker = $this->app->make('queue.worker'); + $work = static fn () => $worker->runNextJob('database', 'corpus-live', new WorkerOptions(sleep: 0)); + $count = true; + $work(); + $prefix = $run->historyEvents() + ->get() + ->map->getRawOriginal() + ->all(); + Carbon::setTestNow(now()->addSeconds(2)); + $work(); + $work(); + $this->assertSame('completed', $stub->refresh()->status()); + $this->assertSame([ + 'waited' => true, + 'workflow_id' => $stub->id(), + 'run_id' => $run->id, + ], $stub->output()); + $this->assertSame( + 1, + $constructions, + 'The committed timer continuation must not reconstruct its completed prefix.' + ); + $this->assertSame( + $prefix, + $run->historyEvents() + ->limit(count($prefix)) + ->get() + ->map->getRawOriginal() + ->all() + ); + $this->assertSame($deadlines, [$run->fresh()->getRawOriginal('execution_deadline_at'), + $run->fresh() + ->getRawOriginal('run_deadline_at')]); + $this->assertSame(3, $run->tasks()->count()); + $this->assertSame(3, $run->tasks()->where('status', 'completed')->where('attempt_count', 1)->count()); + $this->assertSame(0, (int) $run->tasks()->sum('repair_count')); + } finally { + Carbon::setTestNow(); + Queue::swap($previousQueue); + config($previous); + DB::table('jobs')->where('queue', 'corpus-live')->delete(); + } + } + public function testFixturesExecuteThroughDeclaredReplayConsumers(): void { self::stopWorkers(); @@ -92,6 +186,11 @@ public function testFixturesExecuteThroughDeclaredReplayConsumers(): void foreach ($this->fixtures() as $fixture) { $this->executeColdReplayFixture($fixture); + if ($fixture['id'] === 'embedded-committed-timer-continuation') { + // Its positive executor path runs in the isolated queue test above. + continue; + } + if ($fixture['id'] === 'embedded-local-activity-retry-queue-delivery') { $this->assertEmbeddedLocalRetryQueuesItsOriginalTask($fixture); continue; diff --git a/tests/Feature/V2/V2TaskDispatchTest.php b/tests/Feature/V2/V2TaskDispatchTest.php index 32290dca7..231a6bb94 100644 --- a/tests/Feature/V2/V2TaskDispatchTest.php +++ b/tests/Feature/V2/V2TaskDispatchTest.php @@ -190,7 +190,7 @@ public function testQueuedTimerAndLocalWritersUseConfiguredWorkflowTaskLease(): $workflowClaim = new ReflectionMethod(RunWorkflowTask::class, 'claimTask'); $workflowClaim->setAccessible(true); - $this->assertSame([true, null], $workflowClaim->invoke(new RunWorkflowTask($workflowTask->id))); + $this->assertSame([true, null, 1], $workflowClaim->invoke(new RunWorkflowTask($workflowTask->id))); $this->assertTrue($workflowTask->fresh()->lease_expires_at->equalTo($now->copy()->addSeconds(8))); $timerRun = $this->createWaitingRun('01J00000000000000000TIMER'); diff --git a/tests/Fixtures/V2/ReplayRegression/embedded-committed-timer-continuation.json b/tests/Fixtures/V2/ReplayRegression/embedded-committed-timer-continuation.json new file mode 100644 index 000000000..c6caf22cf --- /dev/null +++ b/tests/Fixtures/V2/ReplayRegression/embedded-committed-timer-continuation.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://raw.githubusercontent.com/durable-workflow/.github/main/regression-corpus/evidence-schema.json", + "fixture_schema": "durable-workflow.replay-regression/v1", + "id": "embedded-committed-timer-continuation", + "protocol_version": "1.20", + "bindings": ["php"], + "consumers": ["workflow-fiber-runner", "workflow-executor"], + "workflow": { + "type": "Tests\\Fixtures\\V2\\TestTimerWorkflow", + "arguments": [1], + "payload_codec": "avro" + }, + "history": [{ + "sequence": 1, + "event_type": "WorkflowStarted", + "payload": {}, + "recorded_at": "2026-10-17T15:15:00.123456Z" + }], + "expected": { + "completed": false, + "result": null, + "commands": [{"type": "start_timer", "delay_seconds": 1}] + } +} diff --git a/tests/Fixtures/V2/TestContinuationGrowthActivity.php b/tests/Fixtures/V2/TestContinuationGrowthActivity.php new file mode 100644 index 000000000..0ec90d86b --- /dev/null +++ b/tests/Fixtures/V2/TestContinuationGrowthActivity.php @@ -0,0 +1,26 @@ +insert([ + 'run_id' => $this->runId(), + 'activity_id' => $this->activityId(), + 'round' => $round, + ]); + + return [ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ]; + } +} diff --git a/tests/Fixtures/V2/TestContinuationGrowthWorkflow.php b/tests/Fixtures/V2/TestContinuationGrowthWorkflow.php new file mode 100644 index 000000000..837dc07f8 --- /dev/null +++ b/tests/Fixtures/V2/TestContinuationGrowthWorkflow.php @@ -0,0 +1,36 @@ +toJSON()]; + $value = null; + for ($round = 1; $round <= $rounds; ++$round) { + ++self::$roundVisits; + $value = Workflow::activity(TestContinuationGrowthActivity::class, $round); + $clocks[] = Workflow::now()->toJSON(); + Workflow::timer(1); + $clocks[] = Workflow::now()->toJSON(); + } + Workflow::timer(1); + $clocks[] = Workflow::now()->toJSON(); + + return [ + 'rounds' => $rounds, + 'value' => $value, + 'clocks' => $clocks, + ]; + } +} diff --git a/tests/Fixtures/V2/TestContinuationProcessActivity.php b/tests/Fixtures/V2/TestContinuationProcessActivity.php new file mode 100644 index 000000000..ddd31e6be --- /dev/null +++ b/tests/Fixtures/V2/TestContinuationProcessActivity.php @@ -0,0 +1,21 @@ + 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ]; + } +} diff --git a/tests/Fixtures/V2/TestContinuationProcessWorkflow.php b/tests/Fixtures/V2/TestContinuationProcessWorkflow.php new file mode 100644 index 000000000..1c0bb1648 --- /dev/null +++ b/tests/Fixtures/V2/TestContinuationProcessWorkflow.php @@ -0,0 +1,19 @@ + + */ + private array $workers = []; + + protected function setUp(): void + { + $this->directory = sys_get_temp_dir() . '/dw-growth-' . bin2hex(random_bytes(8)); + mkdir($this->directory, 0700); + touch($this->directory . '/database.sqlite'); + parent::setUp(); + $this->artisan('migrate', [ + '--force' => true, + ])->run(); + DB::statement('CREATE TABLE jobs (id INTEGER PRIMARY KEY AUTOINCREMENT, + queue TEXT NOT NULL, payload TEXT NOT NULL, attempts INTEGER NOT NULL, + reserved_at INTEGER, available_at INTEGER NOT NULL, created_at INTEGER NOT NULL)'); + DB::statement('CREATE TABLE failed_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL UNIQUE, connection TEXT NOT NULL, queue TEXT NOT NULL, + payload TEXT NOT NULL, exception TEXT NOT NULL, failed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)'); + DB::statement('CREATE TABLE continuation_effects (id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id TEXT NOT NULL, activity_id TEXT NOT NULL, round INTEGER NOT NULL)'); + } + + protected function tearDown(): void + { + foreach ($this->workers as $worker) { + if ($worker->isRunning()) { + $worker->stop(0, 9); + } + } + if ($this->status()->isFailure() || $this->status()->isError()) { + $this->reportFailedState(); + } + parent::tearDown(); + (new Filesystem())->deleteDirectory($this->directory); + } + + public static function workloads(): iterable + { + $setup = getenv('PROFILE_EMBEDDED_CONTINUATION') === 'setup'; + foreach ($setup ? [1] : [1, 19, 199] as $rounds) { + foreach ([1, 4] as $workers) { + foreach ([1, 8] as $concurrent) { + foreach ($setup ? [1] : [1, 2, 3] as $repetition) { + foreach ($repetition % 2 === 0 ? [true, false] : [false, true] as $retained) { + yield sprintf( + '%d-events_%d-workers_%d-runs_%s_repeat-%d', + 5 * $rounds + 5, + $workers, + $concurrent, + $retained ? 'retained' : 'replay', + $repetition + ) + => [$rounds, $workers, $concurrent, $retained, $repetition]; + } + } + } + } + } + } + + #[DataProvider('workloads')] + public function testActualQueueGrowth( + int $rounds, + int $workerCount, + int $concurrent, + bool $retained, + int $repetition, + ): void { + $mode = getenv('PROFILE_EMBEDDED_CONTINUATION'); + if (! in_array($mode, ['setup', 'measure'], true)) { + $this->markTestSkipped('Explicit isolated growing-history qualification only.'); + } + $budget = $mode === 'setup' ? ($concurrent === 1 ? 30 : 120) : 3600; + $observers = getenv('DWF_GROWTH_OBSERVERS') !== 'off'; + $limits = [ + 'cpu_max' => trim((string) @file_get_contents('/sys/fs/cgroup/cpu.max')), + 'memory_max' => trim((string) @file_get_contents('/sys/fs/cgroup/memory.max')), + 'swap_max' => trim((string) @file_get_contents('/sys/fs/cgroup/memory.swap.max')), + ]; + if ($mode === 'measure') { + $this->assertSame('1073741824', $limits['memory_max']); + $this->assertSame('0', $limits['swap_max']); + $this->assertSame('100000 100000', $limits['cpu_max']); + $this->assertMatchesRegularExpression( + '/^[0-9]+$/D', + trim((string) @file_get_contents('/sys/fs/cgroup/memory.peak')), + 'Qualified aggregate peak memory requires the cgroup peak counter.' + ); + } + $boot = hrtime(true); + for ($index = 0; $index < $workerCount; ++$index) { + $this->workers[] = $this->startWorker($index, $retained, $workerCount, $budget); + } + $startupDeadline = hrtime(true) + 30_000_000_000; + while (count(glob($this->directory . '/worker-*.ready')) !== $workerCount) { + $this->assertWorkersRunning(); + $this->assertLessThan($startupDeadline, hrtime(true), 'Original 30-second startup bound elapsed.'); + usleep(50_000); + } + $startupWall = hrtime(true) - $boot; + $ready = []; + foreach ($this->workers as $index => $worker) { + $ready[] = $this->readReport('/worker-' . $index . '.ready'); + $this->assertSame($worker->getPid(), $ready[$index]['pid']); + $this->assertSame($observers, $ready[$index]['observers_enabled']); + $this->assertSame(ContinuationGrowthEnvironment::loadedSources(), $ready[$index]['loaded_sources']); + $this->assertSame( + hash_file('sha256', dirname(__DIR__, 2) . '/composer.lock'), + $ready[$index]['lock_sha256'] + ); + } + $deadline = hrtime(true) + $budget * 1_000_000_000; + $originals = []; + for ($index = 0; $index < $concurrent; ++$index) { + $stub = WorkflowStub::make(TestContinuationGrowthWorkflow::class, 'growth-' . $index, 'growth'); + $stub->start(new StartOptions(executionTimeoutSeconds: $budget, runTimeoutSeconds: $budget), $rounds); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $originals[] = [ + 'stub' => $stub, + 'run' => $run, + 'deadlines' => [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')], + 'prefix' => $run->historyEvents() + ->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(), + ]; + } + $started = hrtime(true); + $cgroupCpu = $this->cgroupCpu(); + touch($this->directory . '/start'); + try { + while (count(glob($this->directory . '/completed-*')) !== $concurrent) { + $this->assertWorkersRunning(); + $this->assertLessThan($deadline, hrtime(true), 'Original cohort observation budget elapsed.'); + usleep(50_000); + } + $shutdownDeadline = hrtime(true) + 15_000_000_000; + while (DB::table('jobs')->exists()) { + $this->assertWorkersRunning(); + $this->assertLessThan( + $shutdownDeadline, + hrtime(true), + 'Original 15-second drain bound elapsed with queued transport work.' + ); + usleep(50_000); + } + touch($this->directory . '/stop'); + while (array_filter($this->workers, static fn (Process $worker): bool => $worker->isRunning()) !== []) { + foreach ($this->workers as $worker) { + $worker->checkTimeout(); + } + $this->assertLessThan( + $shutdownDeadline, + hrtime(true), + 'Original 15-second graceful drain bound elapsed.' + ); + usleep(50_000); + } + } catch (Throwable $failure) { + echo 'CONTINUATION_GROWTH_FAILED=' . json_encode([ + 'rounds' => $rounds, + 'workers' => $workerCount, + 'runs' => $concurrent, + 'retained' => $retained, + 'completed_markers' => count(glob($this->directory . '/completed-*')), + 'unfinished' => $concurrent - count(glob($this->directory . '/completed-*')), + 'elapsed_ns' => hrtime(true) - $started, + 'workers_before_cleanup' => $this->failedWorkerReports(), + 'cgroup_workload_cpu_us' => $this->cgroupCpu() - $cgroupCpu, + 'cgroup_peak_memory_bytes' => trim((string) @file_get_contents('/sys/fs/cgroup/memory.peak')), + 'message' => $failure->getMessage(), + ], JSON_THROW_ON_ERROR) . PHP_EOL; + throw $failure; + } + $elapsed = hrtime(true) - $started; + $cgroupCpu = $this->cgroupCpu() - $cgroupCpu; + $reports = []; + foreach ($this->workers as $index => $worker) { + $this->assertSame(0, $worker->getExitCode(), $this->diagnostic($worker)); + $reports[] = $this->readReport('/worker-' . $index . '.json'); + $this->assertSame(0, $reports[$index]['exit']); + $this->assertSame($observers, $reports[$index]['observers_enabled']); + $this->assertSame(0, $reports[$index]['cache']['entries']); + if ($observers) { + $this->assertLessThanOrEqual(4, $reports[$index]['observed_peak_entries']); + $this->assertGreaterThanOrEqual(0, $reports[$index]['history_selects']); + $this->assertLessThanOrEqual($reports[$index]['queries'], $reports[$index]['history_selects']); + } else { + foreach (['queries', 'query_elapsed_ms', 'history_selects', 'history_select_elapsed_ms', + 'task_queue_delay', 'timer_lateness', + 'observed_peak_entries'] as $metric) { + $this->assertNull($reports[$index][$metric]); + } + } + } + $audit = []; + foreach ($originals as $original) { + $run = $original['run']->fresh(); + $stub = $original['stub']->refresh(); + $this->assertSame('completed', $stub->status()); + $output = $stub->output(); + $this->assertSame($rounds, $output['rounds']); + $this->assertSame([ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ], $output['value']); + $events = $run->historyEvents() + ->orderBy('sequence') + ->get(); + $this->assertCount(5 * $rounds + 5, $events); + $this->assertSame(range(1, count($events)), $events->pluck('sequence')->all()); + $this->assertSame( + $original['prefix'], + $events->take(count($original['prefix']))->map->getRawOriginal()->all() + ); + $this->assertSame($original['deadlines'], [$run->getRawOriginal('execution_deadline_at'), + $run->getRawOriginal('run_deadline_at')]); + $clocks = [$run->started_at->toJSON(), ...$events->whereIn('event_type', [ + HistoryEventType::ActivityCompleted, HistoryEventType::TimerFired, + ])->map->recorded_at->map->toJSON() + ->values() + ->all()]; + $this->assertSame($clocks, $output['clocks']); + $effects = DB::table('continuation_effects')->where('run_id', $run->id)->orderBy('round')->get(); + $this->assertCount($rounds, $effects); + $this->assertSame(range(1, $rounds), $effects->pluck('round')->all()); + $this->assertCount($rounds, $effects->pluck('activity_id')->unique()); + $this->assertSame(ActivityExecution::query()->where('workflow_run_id', $run->id) + ->orderBy('sequence') + ->pluck('id') + ->all(), $effects->pluck('activity_id') + ->all()); + $this->assertSame(4 * $rounds + 3, $run->tasks()->where('status', 'completed')->count()); + $this->assertSame([1], $run->tasks()->pluck('attempt_count')->map( + static fn ($attempt): int => (int) $attempt + )->unique() + ->values() + ->all()); + $this->assertSame([1], ActivityExecution::query()->where('workflow_run_id', $run->id) + ->pluck('attempt_count') + ->unique() + ->values() + ->all()); + $audit[] = [ + 'run_id' => $run->id, + 'events' => count($events), + 'effects' => count($effects), + 'output_sha256' => hash('sha256', json_encode($output, JSON_THROW_ON_ERROR)), + 'deadlines' => $original['deadlines'], + ]; + } + $tasks = WorkflowTask::query()->get(); + $this->assertSame(['completed'], $tasks->pluck('status')->map->value->unique()->values()->all()); + $maintenance = array_column($reports, 'task_maintenance'); + foreach ($reports as $report) { + if ($observers) { + $this->assertTrue($report['execution_profile']['balanced']); + $this->assertSame( + $report['entries'], + $report['execution_profile']['phases']['authoring_start']['calls'] ?? 0 + ); + $exclusive = array_sum(array_column($report['execution_profile']['phases'], 'exclusive_cpu_us')); + $this->assertLessThanOrEqual($report['queue_cpu_us'], $exclusive); + } else { + $this->assertNull($report['execution_profile']); + } + } + $redispatches = array_sum(array_column($maintenance, 'ready_redispatches')); + $this->assertSame(0, array_sum(array_column($maintenance, 'lease_recoveries'))); + $this->assertSame(0, array_sum(array_column($maintenance, 'other_repairs'))); + $this->assertSame((int) $tasks->sum('repair_count'), $redispatches); + if ($observers) { + $this->assertGreaterThan(0, array_sum(array_column($reports, 'history_selects'))); + $this->assertSame( + (int) $tasks->sum('attempt_count'), + array_sum(array_column(array_column($reports, 'task_queue_delay'), 'count')) + ); + $this->assertSame( + ($rounds + 1) * $concurrent, + array_sum(array_column(array_column($reports, 'timer_lateness'), 'count')) + ); + } + $this->assertSame(0, DB::table('failed_jobs')->count()); + $this->assertSame(0, DB::table('jobs')->count()); + if (! $retained) { + $this->assertSame((2 * $rounds + 2) * $concurrent, array_sum(array_column($reports, 'entries'))); + } + $peakMemory = @file_get_contents('/sys/fs/cgroup/memory.peak'); + echo 'CONTINUATION_GROWTH_REPORT=' . json_encode([ + 'schema' => 'embedded-continuation-growth-v1', + 'mode' => $mode, + 'observers_enabled' => $observers, + 'events_per_run' => 5 * $rounds + 5, + 'rounds' => $rounds, + 'worker_count' => $workerCount, + 'concurrent_runs' => $concurrent, + 'retained' => $retained, + 'repetition' => $repetition, + 'original_budget_seconds' => $budget, + 'limits' => $limits, + 'startup_wall_ns' => $startupWall, + 'worker_ready' => $ready, + 'workload_wall_ns' => $elapsed, + 'cgroup_workload_cpu_us' => $cgroupCpu, + 'cgroup_peak_memory_bytes' => $peakMemory === false ? null : (int) trim($peakMemory), + 'worker_reports' => $reports, + 'audit' => $audit, + 'ready_task_redispatches' => $redispatches, + 'errors' => 0, + 'unfinished' => 0, + ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES) . PHP_EOL; + } + + protected function getPackageProviders($app): array + { + return ContinuationGrowthEnvironment::providers(); + } + + protected function defineEnvironment($app): void + { + ContinuationGrowthEnvironment::configure($app, $this->directory, false); + } + + private function startWorker(int $index, bool $retained, int $workers, int $budget): Process + { + $worker = new Process([PHP_BINARY, dirname(__DIR__, 2) . '/vendor/bin/phpunit', + dirname(__DIR__) . '/Support/ContinuationGrowthWorkerTest.php', + '--filter', 'testDaemon', + '--cache-directory', $this->directory . '/phpunit-' . $index], dirname(__DIR__, 2), [ + 'DWF_GROWTH_DIRECTORY' => $this->directory, + 'DWF_GROWTH_WORKER' => (string) $index, + 'DWF_GROWTH_RETAINED' => $retained ? '1' : '0', + 'DWF_GROWTH_BUDGET' => (string) $budget, + 'DWF_GROWTH_MEMORY' => (string) intdiv(1024, $workers), + 'DWF_GROWTH_OBSERVERS' => getenv('DWF_GROWTH_OBSERVERS') === 'off' ? 'off' : 'on', + 'XDEBUG_MODE' => 'off', + 'APP_SERVICES_CACHE' => $this->directory . '/services-' . $index . '.php', + 'APP_PACKAGES_CACHE' => $this->directory . '/packages-' . $index . '.php', + ]); + $worker->setTimeout($budget + 45); + $worker->start(); + + return $worker; + } + + private function assertWorkersRunning(): void + { + foreach ($this->workers as $worker) { + $worker->checkTimeout(); + $this->assertTrue($worker->isRunning(), $this->diagnostic($worker)); + } + } + + private function diagnostic(Process $worker): string + { + return substr($worker->getOutput(), -4096) . substr($worker->getErrorOutput(), -4096); + } + + private function failedWorkerReports(): array + { + $reports = []; + foreach ($this->workers as $index => $worker) { + $path = $this->directory . '/worker-' . $index . '.json'; + $reports[] = [ + 'index' => $index, + 'running' => $worker->isRunning(), + 'exit_code' => $worker->getExitCode(), + // A missing report is not zero CPU/RSS and is not a successful drain. + 'final_report' => is_file($path) ? json_decode((string) file_get_contents($path), true) : null, + ]; + } + + return $reports; + } + + private function reportFailedState(): void + { + try { + $fields = ['id', 'workflow_run_id', 'task_type', 'status', 'attempt_count', 'repair_count', + 'available_at', 'leased_at', 'lease_expires_at', 'last_dispatch_attempt_at', + 'last_dispatched_at', 'last_claim_failed_at', 'last_claim_error', 'last_error']; + $tasks = WorkflowTask::query()->where('repair_count', '>', 0)->orderBy('id')->limit(16)->get(); + echo 'CONTINUATION_GROWTH_AUDIT_FAILURE=' . json_encode([ + 'test' => $this->nameWithDataSet(), + 'run_count' => WorkflowRun::query()->count(), + 'completed_runs' => WorkflowRun::query()->where('status', 'completed')->count(), + 'history_events' => WorkflowHistoryEvent::query()->count(), + 'activity_effects' => DB::table('continuation_effects')->count(), + 'task_count' => WorkflowTask::query()->count(), + 'completed_tasks' => WorkflowTask::query()->where('status', 'completed')->count(), + 'repaired_task_count' => WorkflowTask::query()->where('repair_count', '>', 0)->count(), + 'repaired_tasks' => $tasks->map(static function (WorkflowTask $task) use ($fields): array { + $row = array_intersect_key($task->getRawOriginal(), array_flip($fields)); + foreach (['last_error', 'last_claim_error'] as $field) { + if (is_string($row[$field] ?? null)) { + $row[$field] = substr($row[$field], 0, 1024); + } + } + + return $row; + })->all(), + 'failed_jobs' => DB::table('failed_jobs')->count(), + 'queue_rows' => DB::table('jobs')->orderBy('id')->limit(8)->get()->map(static function ($job): array { + $payload = (string) $job->payload; + $metadata = json_decode($payload, true); + + return [ + 'id' => $job->id, + 'queue' => $job->queue, + 'attempts' => $job->attempts, + 'reserved_at' => $job->reserved_at, + 'available_at' => $job->available_at, + 'display_name' => is_array($metadata) ? ($metadata['displayName'] ?? null) : null, + 'payload_bytes' => strlen($payload), + 'payload_sha256' => hash('sha256', $payload), + ]; + })->all(), + 'failed_job_errors' => DB::table('failed_jobs')->orderBy('id')->limit(2)->get() + ->map(static fn ($job): array => [ + 'id' => $job->id, + 'queue' => $job->queue, + 'exception' => substr((string) $job->exception, 0, 2048), + ])->all(), + ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES) . PHP_EOL; + } catch (Throwable $failure) { + echo 'CONTINUATION_GROWTH_AUDIT_UNAVAILABLE=' . substr($failure->getMessage(), 0, 1024) . PHP_EOL; + } + } + + private function readReport(string $suffix): array + { + return json_decode(file_get_contents($this->directory . $suffix), true, flags: JSON_THROW_ON_ERROR); + } + + private function cgroupCpu(): int + { + $stat = (string) @file_get_contents('/sys/fs/cgroup/cpu.stat'); + + return preg_match('/^usage_usec ([0-9]+)$/m', $stat, $matches) === 1 ? (int) $matches[1] : 0; + } +} diff --git a/tests/Performance/continuation/COMPOSER-INPUT.lock b/tests/Performance/continuation/COMPOSER-INPUT.lock new file mode 100644 index 000000000..8eca03f64 --- /dev/null +++ b/tests/Performance/continuation/COMPOSER-INPUT.lock @@ -0,0 +1,9764 @@ +{ + "_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": "1570878417d6b126b92cf3f00dc26b49", + "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": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "2effe05d2177c451b86c6a073196a4034c02f211" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/2effe05d2177c451b86c6a073196a4034c02f211", + "reference": "2effe05d2177c451b86c6a073196a4034c02f211", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "phpstan/phpstan": "2.2.13", + "phpstan/phpstan-phpunit": "2.0.18", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-09-12T10:28:18+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "bf72f821b9952722ea9f9c3912f85946e1af9e3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/bf72f821b9952722ea9f9c3912f85946e1af9e3d", + "reference": "bf72f821b9952722ea9f9c3912f85946e1af9e3d", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "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/2.1.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:23+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.1", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "4189372ffeff09bf14f731f67a9de90c7b413ebe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/4189372ffeff09bf14f731f67a9de90c7b413ebe", + "reference": "4189372ffeff09bf14f731f67a9de90c7b413ebe", + "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": "^2.1.31", + "phpunit/phpunit": "^11.5.43 || ^12.4" + }, + "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.1" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2026-10-19T17:53:57+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": "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": "8.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b920e5fcaf81d761b8fabe600ca90999beaefdb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b920e5fcaf81d761b8fabe600ca90999beaefdb7", + "reference": "b920e5fcaf81d761b8fabe600ca90999beaefdb7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^3.1", + "guzzlehttp/psr7": "^3.2", + "php": "^7.4 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php82": "^1.27" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "4.0.1", + "guzzlehttp/test-server": "^1.1", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^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": { + "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/8.3.0" + }, + "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-10-18T13:38:43+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "094d0ec418eb3b1411ea271a180ec95cae6ae933" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/094d0ec418eb3b1411ea271a180ec95cae6ae933", + "reference": "094d0ec418eb3b1411ea271a180ec95cae6ae933", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^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/3.1.0" + }, + "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-10-18T12:48:16+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "cdc139396207a5eba10c242d241fb014cb098390" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/cdc139396207a5eba10c242d241fb014cb098390", + "reference": "cdc139396207a5eba10c242d241fb014cb098390", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^2.0", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php82": "^1.27" + }, + "provide": { + "psr/http-factory-implementation": "1.1", + "psr/http-message-implementation": "2.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", + "php-http/psr7-integration-tests": "^1.5.1", + "phpunit/phpunit": "^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/3.2.0" + }, + "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-10-17T23:57:19+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/7a466ad606491eb6528c717482f7cca77f1851f3", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "symfony/polyfill-php80": "^1.25" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^9.6.34", + "uri-template/tests": "1.0.2" + }, + "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/v2.0.1" + }, + "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-24T17:13:02+00:00" + }, + { + "name": "laravel/framework", + "version": "v13.38.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "c42f9abd4a3d2f9a3cd6be109255ddce8092ceb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/c42f9abd4a3d2f9a3cd6be109255ddce8092ceb8", + "reference": "c42f9abd4a3d2f9a3cd6be109255ddce8092ceb8", + "shasum": "" + }, + "require": { + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^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 || ^8.0", + "guzzlehttp/promises": "^2.0.3 || ^3.0", + "guzzlehttp/psr7": "^2.9 || ^3.0", + "guzzlehttp/uri-template": "^1.0 || ^2.0", + "laravel/prompts": "^0.3.25", + "laravel/serializable-closure": "^2.0.10", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.10", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.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/image": "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", + "intervention/image": "^4.2", + "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": "^11.6.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "2.3.1", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "rector/rector": "2.6.3", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/mercure": "^0.8.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0", + "web-token/jwt-library": "^4.1" + }, + "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).", + "intervention/image": "Required to use the image processing features (^4.2).", + "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 (^7.0 || ^8.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.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).", + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/mercure": "Required to use the Mercure broadcast driver (^0.8).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0).", + "web-token/jwt-library": "Required to sign Mercure JWTs and use end-to-end encrypted channels (^4.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.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-10-20T10:45:58+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.4", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "40821b7606d8d946bac57dbd2290076c722e3a65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/40821b7606d8d946bac57dbd2290076c722e3a65", + "reference": "40821b7606d8d946bac57dbd2290076c722e3a65", + "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-10-17T19:59:50+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.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "fcc872a3d872aa13f935e0cd1887b9abb06da07f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/fcc872a3d872aa13f935e0cd1887b9abb06da07f", + "reference": "fcc872a3d872aa13f935e0cd1887b9abb06da07f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/uri-interfaces": "^7.9.0", + "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.9.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-10-16T15:19:33+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "eb5f6d24e6a4fe0a8e9c78ded4eb1dfff1404f58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/eb5f6d24e6a4fe0a8e9c78ded4eb1dfff1404f58", + "reference": "eb5f6d24e6a4fe0a8e9c78ded4eb1dfff1404f58", + "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.9.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-10-16T15:19:33+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.15.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "217d38553209bdb44e01590d9e9a2e4df8354cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/217d38553209bdb44e01590d9e9a2e4df8354cce", + "reference": "217d38553209bdb44e01590d9e9a2e4df8354cce", + "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/polyfill-time": "^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-18T18:27:53+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": "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": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+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-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-php82", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/843bbe8b2a7934307b749751eede0f111f5d1cfc", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/80ccff923a8d61f73ebe9da0aa94e25232f004ce", + "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/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-php86", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/8840df45b24f33a49dc419fa18dc0057231ae1d8", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8", + "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\\Php86\\": "" + }, + "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.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/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-time", + "version": "v1.42.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-time.git", + "reference": "ac9f1ed802dd58f37e720388130c088434aa09fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-time/zipball/ac9f1ed802dd58f37e720388130c088434aa09fc", + "reference": "ac9f1ed802dd58f37e720388130c088434aa09fc", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Time\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Philippe Nyamagana Butera", + "email": "nyamsprod@gmail.com" + }, + { + "name": "Tim Düsterhus", + "email": "tim@tideways-gmbh.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Time API added to PHP 8.6", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "duration", + "polyfill", + "portable", + "shim", + "time" + ], + "support": { + "source": "https://github.com/symfony/polyfill-time/tree/v1.42.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-08-07T13:08:13+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/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": [ + { + "name": "aws/aws-crt-php", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" + }, + "time": "2024-10-18T22:15:13+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.401.3", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "7b8541e8a4f09cf4375eeef4de6c638dfb2dadbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7b8541e8a4f09cf4375eeef4de6c638dfb2dadbb", + "reference": "7b8541e8a4f09cf4375eeef4de6c638dfb2dadbb", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^7.8.2 || ^8.0", + "guzzlehttp/promises": "^2.0.3 || ^3.0", + "guzzlehttp/psr7": "^2.6.3 || ^3.0", + "mtdowling/jmespath.php": "^2.9.1", + "php": ">=8.1", + "psr/http-message": "^1.0 || ^2.0", + "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^2.7.8", + "dms/phpunit-arraysubset-asserts": "^v0.5.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-sockets": "*", + "phpunit/phpunit": "^10.0", + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-pcntl": "To use client-side monitoring", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + }, + "exclude-from-classmap": [ + "src/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "https://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "https://aws.amazon.com/sdk-for-php", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://github.com/aws/aws-sdk-php/discussions", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.401.3" + }, + "time": "2026-10-19T18:11:47+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": "doctrine/dbal", + "version": "3.10.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "c95589d775a0b2e543467d40f8c3ecccf586f2b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/c95589d775a0b2e543467d40f8c3ecccf586f2b4", + "reference": "c95589d775a0b2e543467d40f8c3ecccf586f2b4", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/cache": "< 1.11" + }, + "require-dev": { + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.34", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^5.4|^6.0|^7.0|^8.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "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" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.10.6" + }, + "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%2Fdbal", + "type": "tidelift" + } + ], + "time": "2026-07-21T12:57:49+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "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" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" + }, + "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%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2026-01-29T07:11:08+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.7", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "50670ba239926c50f2073e868f6672f775cd57ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/50670ba239926c50f2073e868f6672f775cd57ec", + "reference": "50670ba239926c50f2073e868f6672f775cd57ec", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3 || ^10.5.58", + "symfony/var-dumper": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.7" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2026-10-18T13:17:38+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b61cd040da1a4925bc90a51c074f5297e7c0fa52", + "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v3.0.0" + }, + "time": "2026-03-17T11:56:53+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-05-20T22:24:57+00:00" + }, + { + "name": "laravel/tinker", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "4faba77764bd33411735936acdf30446d058c78b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", + "shasum": "" + }, + "require": { + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5|^11.5" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v3.0.2" + }, + "time": "2026-03-17T14:54:13+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.15", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/967a801bd188989a5669bd280f252d51c0fdc9ee", + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0 || ^3.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6.36", + "symplify/easy-coding-standard": "^13.2.17" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2026-08-19T19:37:52+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.9.2", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/2157c5e50e813ec6a96c1eed3be7f64a20fb32a8", + "reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.52" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.9-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "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" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.9.2" + }, + "time": "2026-07-06T18:56:19+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.5", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/fb53eacd509a1d303858e2d20cfebf2d630254ec", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.14 || ^8.1.1" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.10.0", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.20.0", + "laravel/pint": "^1.29.3", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.3.5", + "pestphp/pest": "^3.8.5 || ^4.7.5 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.1.2 || ^9.3.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-07-15T19:09:14+00:00" + }, + { + "name": "orchestra/canvas", + "version": "v11.1.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/canvas.git", + "reference": "d13e6ec66cb1a285fcc7f092c804abaaf2e24ede" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/canvas/zipball/d13e6ec66cb1a285fcc7f092c804abaaf2e24ede", + "reference": "d13e6ec66cb1a285fcc7f092c804abaaf2e24ede", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "illuminate/console": "^13.23.0", + "illuminate/database": "^13.23.0", + "illuminate/filesystem": "^13.23.0", + "illuminate/support": "^13.23.0", + "orchestra/canvas-core": "^11.0.0", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "orchestra/testbench-core": "^11.0.0", + "php": "^8.3", + "symfony/yaml": "^7.4.0|^8.0.0" + }, + "require-dev": { + "laravel/doctor": "^0.1.0", + "laravel/framework": "^13.23.0", + "laravel/pint": "^1.24", + "mockery/mockery": "^1.6.10", + "phpstan/phpstan": "^2.1.14", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0" + }, + "bin": [ + "canvas" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Orchestra\\Canvas\\LaravelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Canvas\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Code Generators for Laravel Applications and Packages", + "support": { + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas/tree/v11.1.0" + }, + "time": "2026-08-04T15:06:43+00:00" + }, + { + "name": "orchestra/canvas-core", + "version": "v11.0.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/canvas-core.git", + "reference": "88d091ff989748e2ca447bca0cd06ab14671ba82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/canvas-core/zipball/88d091ff989748e2ca447bca0cd06ab14671ba82", + "reference": "88d091ff989748e2ca447bca0cd06ab14671ba82", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "illuminate/console": "^13.0", + "illuminate/support": "^13.0", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "php": "^8.3" + }, + "require-dev": { + "laravel/framework": "^13.0", + "laravel/pint": "^1.24", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^11.0", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Orchestra\\Canvas\\Core\\LaravelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Canvas\\Core\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Code Generators Builder for Laravel Applications and Packages", + "support": { + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas-core/tree/v11.0.0" + }, + "time": "2026-03-16T15:10:50+00:00" + }, + { + "name": "orchestra/sidekick", + "version": "v1.2.23", + "source": { + "type": "git", + "url": "https://github.com/orchestral/sidekick.git", + "reference": "cbdfe583afc069be2ca6790489e9dda007d14d00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/sidekick/zipball/cbdfe583afc069be2ca6790489e9dda007d14d00", + "reference": "cbdfe583afc069be2ca6790489e9dda007d14d00", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "php": "^8.1", + "symfony/polyfill-php83": "^1.32" + }, + "require-dev": { + "fakerphp/faker": "^1.21", + "laravel/framework": "^10.48.29|^11.44.7|^12.1.1|^13.0", + "laravel/pint": "^1.4", + "mockery/mockery": "^1.5.1", + "orchestra/testbench-core": "^8.37.0|^9.14.0|^10.2.0|^11.0", + "phpstan/phpstan": "2.2.14", + "phpunit/phpunit": "^10.0|^11.0|^12.0|^13.0", + "symfony/process": "^6.0|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Eloquent/functions.php", + "src/Filesystem/functions.php", + "src/Http/functions.php", + "src/functions.php" + ], + "psr-4": { + "Orchestra\\Sidekick\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Packages Toolkit Utilities and Helpers for Laravel", + "support": { + "issues": "https://github.com/orchestral/sidekick/issues", + "source": "https://github.com/orchestral/sidekick/tree/v1.2.23" + }, + "time": "2026-09-29T13:27:26+00:00" + }, + { + "name": "orchestra/testbench", + "version": "v11.3.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench.git", + "reference": "7081895e5bbbdf2fd333fdf34e8ddec6cd4db713" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/7081895e5bbbdf2fd333fdf34e8ddec6cd4db713", + "reference": "7081895e5bbbdf2fd333fdf34e8ddec6cd4db713", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.23", + "laravel/framework": "^13.32.0", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^11.5.0", + "orchestra/workbench": "^11.3.0", + "php": "^8.3", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "symfony/process": "^7.4.5|^8.0.5", + "symfony/yaml": "^7.4|^8.0", + "vlucas/phpdotenv": "^5.6.1" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Laravel Testing Helper for Packages Development", + "homepage": "https://packages.tools/testbench/", + "keywords": [ + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench/tree/v11.3.0" + }, + "time": "2026-09-24T07:19:57+00:00" + }, + { + "name": "orchestra/testbench-core", + "version": "v11.6.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench-core.git", + "reference": "f139a4ee74638dc24e0b6f657b47226c8ba5ab05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/f139a4ee74638dc24e0b6f657b47226c8ba5ab05", + "reference": "f139a4ee74638dc24e0b6f657b47226c8ba5ab05", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "orchestra/sidekick": "~1.1.25|~1.2.22", + "php": "^8.3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-php84": "^1.34.0" + }, + "conflict": { + "brianium/paratest": "<7.3.0|>=8.0.0", + "laravel/framework": "<13.35.0|>=14.0.0", + "laravel/serializable-closure": ">=2.0.0 <2.0.10|>=3.0.0", + "nunomaduro/collision": "<8.9.0|>=9.0.0", + "phpunit/phpunit": "<11.5.50|>=12.0.0 <12.5.8|>=13.5.0" + }, + "require-dev": { + "fakerphp/faker": "^1.24", + "laravel/framework": "^13.35.0", + "laravel/pint": "^1.24", + "laravel/serializable-closure": "^2.0.10", + "mockery/mockery": "^1.6.10", + "phpstan/phpstan": "2.2.16", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "spatie/laravel-ray": "^1.43.6", + "symfony/process": "^7.4.5|^8.0.5", + "symfony/yaml": "^7.4.0|^8.0.0", + "vlucas/phpdotenv": "^5.6.1" + }, + "suggest": { + "brianium/paratest": "Allow using parallel testing (^7.3).", + "ext-pcntl": "Required to use all features of the console signal trapping.", + "fakerphp/faker": "Allow using Faker for testing (^1.23).", + "laravel/framework": "Required for testing (^13.35.0).", + "mockery/mockery": "Allow using Mockery for testing (^1.6).", + "nunomaduro/collision": "Allow using Laravel style tests output and parallel testing (^8.9).", + "orchestra/testbench-dusk": "Allow using Laravel Dusk for testing (^11.0).", + "phpunit/phpunit": "Allow using PHPUnit for testing (^11.5.50|^12.5.8|^13.0.0).", + "symfony/process": "Required to use Orchestra\\Testbench\\remote function (^7.4|^8.0).", + "symfony/yaml": "Required for Testbench CLI (^7.4|^8.0).", + "vlucas/phpdotenv": "Required for Testbench CLI (^5.6.1)." + }, + "bin": [ + "testbench" + ], + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Orchestra\\Testbench\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Testing Helper for Laravel Development", + "homepage": "https://packages.tools/testbench", + "keywords": [ + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench-core" + }, + "time": "2026-10-07T23:31:28+00:00" + }, + { + "name": "orchestra/workbench", + "version": "v11.3.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/workbench.git", + "reference": "660046639af4538d6c56373c9222ca4c93055c42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/workbench/zipball/660046639af4538d6c56373c9222ca4c93055c42", + "reference": "660046639af4538d6c56373c9222ca4c93055c42", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.23", + "laravel/framework": "^13.0.0", + "laravel/pail": "^1.2.5", + "laravel/tinker": "^3.0.0", + "nunomaduro/collision": "^8.9", + "orchestra/canvas": "^11.0.1", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "orchestra/testbench-core": "^11.5.0", + "php": "^8.3", + "symfony/process": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "require-dev": { + "laravel/pint": "^1.22.0", + "mockery/mockery": "^1.6.12", + "phpstan/phpstan": "^2.1.33", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "spatie/laravel-ray": "^1.43.6" + }, + "suggest": { + "ext-pcntl": "Required to use all features of the console signal trapping." + }, + "type": "library", + "autoload": { + "psr-4": { + "Orchestra\\Workbench\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Workbench Companion for Laravel Packages Development", + "keywords": [ + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/workbench/issues", + "source": "https://github.com/orchestral/workbench/tree/v11.3.0" + }, + "time": "2026-09-23T23:52:50+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.3.1", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6155239b488e95d26d1ebd95c6b4bb294d7f09db", + "reference": "6155239b488e95d26d1ebd95c6b4bb294d7f09db", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-10-08T21:09:11+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7440368d94738d54f2c1de24dbd3871793bbb0dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7440368d94738d54f2c1de24dbd3871793bbb0dd", + "reference": "7440368d94738d54f2c1de24dbd3871793bbb0dd", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.9.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0.1", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.37" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-10-03T14:03:50+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a248d1640ab059b075f53a2ef0f9856e864e06b5", + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.33" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-08-25T14:40:53+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "babab73b7ef9fae406f2eb3ee11323a61caa63d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/babab73b7ef9fae406f2eb3ee11323a61caa63d5", + "reference": "babab73b7ef9fae406f2eb3ee11323a61caa63d5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.37" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-10-03T04:51:29+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.40", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "76bd3198f16aeb375a4d5f3626471445aed0966e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/76bd3198f16aeb375a4d5f3626471445aed0966e", + "reference": "76bd3198f16aeb375a4d5f3626471445aed0966e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.8", + "phpunit/php-file-iterator": "^6.0.2", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.1", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", + "sebastian/diff": "^7.0.1", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.3", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.4", + "sebastian/version": "^6.0.1", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.40" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-10-16T05:55:07+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.24", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.24" + }, + "time": "2026-06-29T15:41:09+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-05-17T05:29:34+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "7c65c1e79836812819705b473a90c12399542485" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-05-21T04:45:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.33", + "symfony/process": "^7.4.17" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-08-25T15:35:54+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.26" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:40:20+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-05-20T04:37:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.5.28" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:10:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-05-19T16:22:07+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "82ff822c2edc46724be9f7411d3163021f602773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-05-20T06:45:45+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "9e935a16187e14f39b0e79910dabd614972cdb26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/9e935a16187e14f39b0e79910dabd614972cdb26", + "reference": "9e935a16187e14f39b0e79910dabd614972cdb26", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" + } + ], + "time": "2026-10-04T05:50:08+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd0e3462be6d32a0d130e8423e4940a7688162d8", + "reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "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 basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/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-06T20:05:19+00:00" + }, + { + "name": "symplify/easy-coding-standard", + "version": "11.5.0", + "source": { + "type": "git", + "url": "https://github.com/ecsphp/ecs.git", + "reference": "1d2400f7bfe92e3754ce71f0782f2c0521bade3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ecsphp/ecs/zipball/1d2400f7bfe92e3754ce71f0782f2c0521bade3d", + "reference": "1d2400f7bfe92e3754ce71f0782f2c0521bade3d", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "conflict": { + "friendsofphp/php-cs-fixer": "<3.0", + "squizlabs/php_codesniffer": "<3.6", + "symplify/coding-standard": "<11.3" + }, + "bin": [ + "bin/ecs" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Use Coding Standard with 0-knowledge of PHP-CS-Fixer and PHP_CodeSniffer", + "keywords": [ + "Code style", + "automation", + "fixer", + "static analysis" + ], + "support": { + "issues": "https://github.com/easy-coding-standard/ecs/issues", + "source": "https://github.com/easy-coding-standard/ecs/tree/11.5.0" + }, + "funding": [ + { + "url": "https://www.paypal.me/rectorphp", + "type": "custom" + }, + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2023-06-21T06:26:15+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/tests/Performance/continuation/Dockerfile b/tests/Performance/continuation/Dockerfile new file mode 100644 index 000000000..10a3fe975 --- /dev/null +++ b/tests/Performance/continuation/Dockerfile @@ -0,0 +1,8 @@ +FROM composer@sha256:b09bccd91a78fe8a9ab4b33d707b862e8fe54fec17782e32683ad2a69c46867d + +RUN docker-php-ext-install -j1 pcntl \ + && php -r 'exit(function_exists("pcntl_async_signals") && function_exists("pcntl_signal") ? 0 : 1);' + +WORKDIR /app + +USER 1000:1000 diff --git a/tests/Performance/results/issue-748-20261017/EMBEDDED-CONTINUATION-GROWTH.md b/tests/Performance/results/issue-748-20261017/EMBEDDED-CONTINUATION-GROWTH.md new file mode 100644 index 000000000..c919f76fa --- /dev/null +++ b/tests/Performance/results/issue-748-20261017/EMBEDDED-CONTINUATION-GROWTH.md @@ -0,0 +1,188 @@ +# Initial embedded continuation growth measurements + +These are source measurements for [#748](https://github.com/durable-workflow/workflow/issues/748). +They cover 10 and 100 events with one worker and one concurrent run. A later +1,000-event replay pilot fails its original observation budget. Four subsequent +100-event observer controls pass. Larger-history acceptance, concurrent and +multiworker measurements, repeated overhead controls and published consumers +remain incomplete. No production saving or stable warm-execution release is +established. + +## Source and conditions + +- Source: `b2c2ad3e096dad74ba892303fb9fcb11e025852e`, whole tree + `ddfb3c1c7c03f0e17500d611b1ff583fd6f25cd4`. Product runtime/config/resources + are identical to `edb705df`. +- Dependency lock SHA-256: + `1095bc931ee44f4d445c4321b5b31dd9bf27ba02a828b2598296272837e93c79`. + All 950 checked source/resource/fixture files match before admission. +- PHP 8.5.6, Laravel 13.37.0, SQLite 3.51.2 and actual pcntl support. + Pinned tooling image root filesystem has 15 verified layers. Imported image + identity: `6160add78a3e98b07e84d4c243cd5419e45a5a3b30b3e254fe296a40c62e04f8`. +- Isolated x86_64 Linux 6.8.0-146-generic guest, two logical CPUs on a reported + Intel Xeon Skylake virtual CPU, Docker 29.1.3 and cgroup v2. +- One fresh container per cell: one CPU, 1,073,741,824 bytes aggregate memory, + zero swap and no network. Kernel aggregate peak memory is available. There + are no concurrent application/build containers during measured cells. + Guest-wide CPU steal during the 100-event cells is 0.006–0.107% of reported + CPU time; this does not establish physical-host exclusivity. +- Three fresh repetitions per size and mode. Pair order is replay/retained, + retained/replay, replay/retained. Both modes have the original 3,600-second + workflow/execution/observation bounds, 30-second startup and 15-second drain. +- Real database queue, typed activity effects and one-second timers. Cache + bounds stay at four entries, a nonrenewing 30-second age and a 128 MiB worker + RSS threshold. Handoff and cache reuse are not forced. + +The separate guest functional setup passes eight cohorts / 6,150 assertions. +Its timing is excluded from the comparison. All twelve initial b2 cells pass: +six originals at each size, 660 complete events, 120 activity effects, 516 +completed first-attempt tasks and 132 timers in total. Every original preserves +its admission prefix, full typed result, recorded clocks and original deadlines. +There are zero repairs, errors, failed jobs or unfinished runs. All measured +containers exit zero without OOM. + +## Results + +CPU and wall values below are medians of three independent cells. Memory values +are the range of the complete fresh-container peak, including test startup and +audit; they do not isolate retained-frame bytes. Worker CPU includes executor, +history, projection, dispatch and activity/timer work. + +| Events | Mode | Worker CPU seconds | Queue wall seconds | Container peak MiB | Workflow entries | Round visits | +| --- | --- | ---: | ---: | --- | ---: | ---: | +| 10 | Replay | 1.311 | 7.498 | 143.95–144.04 | 4 | 4 | +| 10 | Retained | 1.317 | 7.500 | 144.04–144.27 | 1 | 1 | +| 100 | Replay | 51.055 | 85.428 | 155.13–155.50 | 40 | 418 | +| 100 | Retained | 44.750 | 80.223 | 155.16–155.50 | 3 | 46 | + +At ten events the CPU and wall ranges overlap. At 100 events, retained worker CPU +is 12.3% lower and wall time 6.1% lower in this instrumented workload. Retention +still performs three cold workflow entries and 37 hits: ordinary age eviction +or publication expiry prevents extending a frame's original 30-second lifetime. +These results do not isolate replay CPU or the listeners' cost. + +At 100 events, worker CPU ranges are 51.010–51.353 seconds for replay and +44.747–45.534 seconds for retention. Time to the test worker's ready barrier is +0.406–0.458 seconds, measured separately from queue execution. This includes +PHPUnit/Testbench boot, source checks and opening SQLite. It is not a stock +application's Artisan startup measurement. + +## SQL, memory and scheduling + +All-SQL statement counts are 858 for ten-event replay and 865 for retention. +At 100 events they are 11,223–11,241 and 11,287–11,309 respectively. These counts +include ordinary queue and maintenance reads, and are not history-only counts. +The later probe adds separate history SELECT counts; that data cannot be +backfilled into these originals. + +Each 100-event cell has 79 task claims and 20 timer firings. Per-cell claim-delay +p50 ranges are 342–384 ms for replay and 372–376 ms for retention; p95 ranges +are 1,293–1,473 ms and 1,391–1,392 ms. Timer-lateness p50 ranges are 1,029–1,334 ms +and 1,227–1,248 ms; p95 ranges are 1,848–1,903 ms and 1,766–1,901 ms. These are +ranges of individual-cell percentiles, not pooled distributions. Timer and queue +delay are included in wall time and remain distinct from CPU. + +Worker peak RSS at 100 events is about 89.6–89.8 MiB for replay and 90.1–90.2 MiB +for retention. Actual worker cache entry counts return to zero at shutdown. +SQL duration measures elapsed client calls, not database CPU; process peaks +are not simultaneous aggregate memory. + +## Listener-overhead control and unfinished work + +The following harness preparation introduces `DWF_GROWTH_OBSERVERS=off`, keeping +the same product runtime and durable correctness checks. It removes optional +SQL, queue-delay, timer-lateness and cache-peak listeners and reports their +unmeasured fields as null. Completion barriers, authored counters, CPU/memory +readings and the final history/effect/deadline audit remain. + +The initial local four-cell control is **failed**: three cells pass, while the +retained four-worker/eight-run cell observes a repaired task and fails the +unchanged zero-repair assertion. A separate diagnostic cohort passes but does +not reproduce or explain that original failure. No repair/deadline policy is +relaxed. The new bounded failure report captures task state before disposable +databases are removed. This is incomplete qualification, not a product-cause claim. + +The final minimally observed local component passes four fresh setup cohorts / +5,449 assertions, including the four-worker/eight-run pair. Its new history-only +SELECT fields and optional metrics are checked independently of the preserved +failed original. Hosted and isolated overhead measurements remain required. +The observer-enabled local pair passes two tests / 632 assertions, including +positive history SELECT counts bounded by all-SQL counts. Style and focused +static analysis pass. Later failure-only progress diagnostics remain separate +from these successful component executions. + +Exact `af19fc58` [PR CI](https://github.com/durable-workflow/workflow/actions/runs/39057111488) +is **failed**: five setup cohorts pass and three reject a worker's zero history +SELECT count. Requiring every worker to read history is an incorrect probe +assumption. The bounded failure report records complete run/task counts, +10/80 history events, one/eight effects and zero repairs for the affected cohorts; +the later semantic audits are not reached. This is separate from the earlier +local repaired-task failure. The correction permits nonnegative per-worker +counts bounded by all SQL, and requires a positive history-read count across +the cohort. It changes no runtime, history, task/repair/deadline policy or workload. + +The corrected local eight-cohort run is also **failed**, with six passing cells +and two distinct failures under the one-CPU limit. The four-worker/single-run +retained cell has seven completed tasks and zero repairs/failed jobs, but leaves +one transport row when workers stop. The four-worker/eight-run replay cell has +eight completed originals, two repaired tasks and one failed queue job; one +retained task diagnostic records SQLite error 5 while updating a run summary. +These observations do not establish the failed job's exception or the first +cohort's outstanding job type. They are not attributed to a current release or +the retained runtime. No zero-repair, failed-job or empty-queue assertion is +removed. The passing four-worker/single-run replay control records per-worker +history SELECT counts `[0, 5, 17, 18]`, confirming why the cohort-level metric +check is needed. + +The later drain correction lets workers consume outstanding transport deliveries +before the stop marker, sharing the original 15-second drain deadline with worker +exit. It retains empty-queue, zero-repair and failed-job checks. The fresh local +eight-cohort component passes 8 tests / 9,235 assertions, with style/static checks +passing. This does not explain or fix the earlier SQLite error. Future failed +audits also record bounded queue identity/availability metadata, payload sizes/ +hashes and up to two truncated queue exceptions without serializing whole databases. + +## Failed 1,000-event pilot + +One fresh replay original on exact `af19fc58` fails its original 3,600-second +observation budget. The image, dependency lock, source checks and one-CPU/1-GiB/ +zero-swap limits are unchanged. Its container runs from 18:16:46.537 to +19:16:48.962 UTC on October 17, 2026. + +The failure audit records one unfinished run, 642 history events, 128 activity +effects, 512 of 513 tasks complete, zero repairs and zero failed jobs. Whole- +container CPU is 3,521.991369 seconds and peak memory is 187,785,216 bytes. +The container exits 1 without OOM. Final history/result/deadline audits are not +reached, and fixture teardown stops the unfinished worker. This original remains +failed; larger-history acceptance and a retained comparison remain open. + +## Single-pair observer controls + +Four fresh 100-event originals on the same `af19fc58` source pass the complete +history, typed-result, original-deadline and queue/task audits. Each uses the same +3,600-second budget, 30-second startup, 15-second drain, pinned image/lock and +aggregate resource limits. Source checks pass before every admission. Order is +replay off/on, followed by retained off/on; each row is one observation. + +| Mode | Optional observers | Worker CPU seconds | Queue wall seconds | Container peak bytes | Workflow entries | Round visits | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| Replay | Off | 50.640 | 87.182 | 162,512,896 | 40 | 418 | +| Replay | On | 51.217 | 89.843 | 162,238,464 | 40 | 418 | +| Retained | Off | 44.179 | 75.722 | 162,316,288 | 3 | 47 | +| Retained | On | 44.198 | 78.703 | 162,299,904 | 3 | 46 | + +All four workers and containers exit zero without OOM, and worker cache entries +return to zero. Observer-off SQL, scheduling and peak-entry fields are null. +The observer-enabled replay/retained cells record 472/551 history SELECTs out of +11,247/11,299 total SQL statements. Both preserve 79 task claims and 20 timer +firings. Retained cells each have 37 cache hits; ordinary nonrenewing age eviction +still occurs, and round visits differ across the original expiry boundaries. + +These single pairs do not estimate overhead at 1,000 events or establish a +general gain. The separate SQLite summary-write correction in #835 is outside +these pinned measurements. The disposable guest is retired after readback. + +The next acceptance includes completed 1,000-event and concurrent/multiworker +cells, repeated overhead measurements, hit/miss/capacity behavior under the +aggregate limit, remaining lifetime/failure cases and published consumers. +All original #748 acceptance boxes remain open and #834 remains draft. diff --git a/tests/Performance/results/issue-748-20261018/COMPOSER-INPUT.lock b/tests/Performance/results/issue-748-20261018/COMPOSER-INPUT.lock new file mode 100644 index 000000000..50658cf83 --- /dev/null +++ b/tests/Performance/results/issue-748-20261018/COMPOSER-INPUT.lock @@ -0,0 +1,9392 @@ +{ + "_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": "728b4ab3aa8805a580b5b8c5e937ac89", + "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": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "2effe05d2177c451b86c6a073196a4034c02f211" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/2effe05d2177c451b86c6a073196a4034c02f211", + "reference": "2effe05d2177c451b86c6a073196a4034c02f211", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "phpstan/phpstan": "2.2.13", + "phpstan/phpstan-phpunit": "2.0.18", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-09-12T10:28:18+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "bf72f821b9952722ea9f9c3912f85946e1af9e3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/bf72f821b9952722ea9f9c3912f85946e1af9e3d", + "reference": "bf72f821b9952722ea9f9c3912f85946e1af9e3d", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "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/2.1.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:23+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": "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": "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": "8.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "93939470950a9b11e2e84204166ef5e048c55fe4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/93939470950a9b11e2e84204166ef5e048c55fe4", + "reference": "93939470950a9b11e2e84204166ef5e048c55fe4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^3.0.2", + "guzzlehttp/psr7": "^3.1", + "php": "^7.4 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php82": "^1.27" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "4.0.1", + "guzzlehttp/test-server": "^1.0.1", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^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": { + "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/8.2.0" + }, + "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-09-06T13:55:09+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "42118e66a53c492effaf92bc357e931985d5c6f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/42118e66a53c492effaf92bc357e931985d5c6f9", + "reference": "42118e66a53c492effaf92bc357e931985d5c6f9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^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/3.0.2" + }, + "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-24T10:00:26+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "cdc139396207a5eba10c242d241fb014cb098390" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/cdc139396207a5eba10c242d241fb014cb098390", + "reference": "cdc139396207a5eba10c242d241fb014cb098390", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^2.0", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php82": "^1.27" + }, + "provide": { + "psr/http-factory-implementation": "1.1", + "psr/http-message-implementation": "2.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", + "php-http/psr7-integration-tests": "^1.5.1", + "phpunit/phpunit": "^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/3.2.0" + }, + "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-10-17T23:57:19+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/7a466ad606491eb6528c717482f7cca77f1851f3", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "symfony/polyfill-php80": "^1.25" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^9.6.34", + "uri-template/tests": "1.0.2" + }, + "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/v2.0.1" + }, + "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-24T17:13:02+00:00" + }, + { + "name": "laravel/framework", + "version": "v13.37.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "685f8d6c2ab4547287500bc80ccdf2ca12635109" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/685f8d6c2ab4547287500bc80ccdf2ca12635109", + "reference": "685f8d6c2ab4547287500bc80ccdf2ca12635109", + "shasum": "" + }, + "require": { + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^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 || ^8.0", + "guzzlehttp/promises": "^2.0.3 || ^3.0", + "guzzlehttp/psr7": "^2.9 || ^3.0", + "guzzlehttp/uri-template": "^1.0 || ^2.0", + "laravel/prompts": "^0.3.25", + "laravel/serializable-closure": "^2.0.10", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.10", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.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/image": "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", + "intervention/image": "^4.2", + "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": "^11.6.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "2.3.1", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "rector/rector": "2.6.3", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/mercure": "^0.8.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0", + "web-token/jwt-library": "^4.1" + }, + "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).", + "intervention/image": "Required to use the image processing features (^4.2).", + "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 (^7.0 || ^8.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.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).", + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/mercure": "Required to use the Mercure broadcast driver (^0.8).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0).", + "web-token/jwt-library": "Required to sign Mercure JWTs and use end-to-end encrypted channels (^4.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.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-10-16T04:59:27+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.4", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "40821b7606d8d946bac57dbd2290076c722e3a65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/40821b7606d8d946bac57dbd2290076c722e3a65", + "reference": "40821b7606d8d946bac57dbd2290076c722e3a65", + "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-10-17T19:59:50+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.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "fcc872a3d872aa13f935e0cd1887b9abb06da07f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/fcc872a3d872aa13f935e0cd1887b9abb06da07f", + "reference": "fcc872a3d872aa13f935e0cd1887b9abb06da07f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/uri-interfaces": "^7.9.0", + "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.9.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-10-16T15:19:33+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "eb5f6d24e6a4fe0a8e9c78ded4eb1dfff1404f58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/eb5f6d24e6a4fe0a8e9c78ded4eb1dfff1404f58", + "reference": "eb5f6d24e6a4fe0a8e9c78ded4eb1dfff1404f58", + "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.9.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-10-16T15:19:33+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": "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": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+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-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-php82", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/843bbe8b2a7934307b749751eede0f111f5d1cfc", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/80ccff923a8d61f73ebe9da0aa94e25232f004ce", + "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/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-php86", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/8840df45b24f33a49dc419fa18dc0057231ae1d8", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8", + "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\\Php86\\": "" + }, + "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.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/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/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": [ + { + "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": "doctrine/dbal", + "version": "3.10.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "c95589d775a0b2e543467d40f8c3ecccf586f2b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/c95589d775a0b2e543467d40f8c3ecccf586f2b4", + "reference": "c95589d775a0b2e543467d40f8c3ecccf586f2b4", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/cache": "< 1.11" + }, + "require-dev": { + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.34", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^5.4|^6.0|^7.0|^8.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "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" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.10.6" + }, + "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%2Fdbal", + "type": "tidelift" + } + ], + "time": "2026-07-21T12:57:49+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "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" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" + }, + "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%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2026-01-29T07:11:08+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.6", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "7d0a2cde892089bb09372f4366e9b960eb3001a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/7d0a2cde892089bb09372f4366e9b960eb3001a9", + "reference": "7d0a2cde892089bb09372f4366e9b960eb3001a9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3 || ^10.5.58", + "symfony/var-dumper": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.6" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2026-10-03T17:03:57+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b61cd040da1a4925bc90a51c074f5297e7c0fa52", + "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v3.0.0" + }, + "time": "2026-03-17T11:56:53+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-05-20T22:24:57+00:00" + }, + { + "name": "laravel/tinker", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "4faba77764bd33411735936acdf30446d058c78b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", + "shasum": "" + }, + "require": { + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5|^11.5" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v3.0.2" + }, + "time": "2026-03-17T14:54:13+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.15", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/967a801bd188989a5669bd280f252d51c0fdc9ee", + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0 || ^3.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6.36", + "symplify/easy-coding-standard": "^13.2.17" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2026-08-19T19:37:52+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.5", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/fb53eacd509a1d303858e2d20cfebf2d630254ec", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.14 || ^8.1.1" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.10.0", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.20.0", + "laravel/pint": "^1.29.3", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.3.5", + "pestphp/pest": "^3.8.5 || ^4.7.5 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.1.2 || ^9.3.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-07-15T19:09:14+00:00" + }, + { + "name": "orchestra/canvas", + "version": "v11.1.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/canvas.git", + "reference": "d13e6ec66cb1a285fcc7f092c804abaaf2e24ede" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/canvas/zipball/d13e6ec66cb1a285fcc7f092c804abaaf2e24ede", + "reference": "d13e6ec66cb1a285fcc7f092c804abaaf2e24ede", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "illuminate/console": "^13.23.0", + "illuminate/database": "^13.23.0", + "illuminate/filesystem": "^13.23.0", + "illuminate/support": "^13.23.0", + "orchestra/canvas-core": "^11.0.0", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "orchestra/testbench-core": "^11.0.0", + "php": "^8.3", + "symfony/yaml": "^7.4.0|^8.0.0" + }, + "require-dev": { + "laravel/doctor": "^0.1.0", + "laravel/framework": "^13.23.0", + "laravel/pint": "^1.24", + "mockery/mockery": "^1.6.10", + "phpstan/phpstan": "^2.1.14", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0" + }, + "bin": [ + "canvas" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Orchestra\\Canvas\\LaravelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Canvas\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Code Generators for Laravel Applications and Packages", + "support": { + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas/tree/v11.1.0" + }, + "time": "2026-08-04T15:06:43+00:00" + }, + { + "name": "orchestra/canvas-core", + "version": "v11.0.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/canvas-core.git", + "reference": "88d091ff989748e2ca447bca0cd06ab14671ba82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/canvas-core/zipball/88d091ff989748e2ca447bca0cd06ab14671ba82", + "reference": "88d091ff989748e2ca447bca0cd06ab14671ba82", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "illuminate/console": "^13.0", + "illuminate/support": "^13.0", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "php": "^8.3" + }, + "require-dev": { + "laravel/framework": "^13.0", + "laravel/pint": "^1.24", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^11.0", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Orchestra\\Canvas\\Core\\LaravelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Canvas\\Core\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Code Generators Builder for Laravel Applications and Packages", + "support": { + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas-core/tree/v11.0.0" + }, + "time": "2026-03-16T15:10:50+00:00" + }, + { + "name": "orchestra/sidekick", + "version": "v1.2.23", + "source": { + "type": "git", + "url": "https://github.com/orchestral/sidekick.git", + "reference": "cbdfe583afc069be2ca6790489e9dda007d14d00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/sidekick/zipball/cbdfe583afc069be2ca6790489e9dda007d14d00", + "reference": "cbdfe583afc069be2ca6790489e9dda007d14d00", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "php": "^8.1", + "symfony/polyfill-php83": "^1.32" + }, + "require-dev": { + "fakerphp/faker": "^1.21", + "laravel/framework": "^10.48.29|^11.44.7|^12.1.1|^13.0", + "laravel/pint": "^1.4", + "mockery/mockery": "^1.5.1", + "orchestra/testbench-core": "^8.37.0|^9.14.0|^10.2.0|^11.0", + "phpstan/phpstan": "2.2.14", + "phpunit/phpunit": "^10.0|^11.0|^12.0|^13.0", + "symfony/process": "^6.0|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Eloquent/functions.php", + "src/Filesystem/functions.php", + "src/Http/functions.php", + "src/functions.php" + ], + "psr-4": { + "Orchestra\\Sidekick\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Packages Toolkit Utilities and Helpers for Laravel", + "support": { + "issues": "https://github.com/orchestral/sidekick/issues", + "source": "https://github.com/orchestral/sidekick/tree/v1.2.23" + }, + "time": "2026-09-29T13:27:26+00:00" + }, + { + "name": "orchestra/testbench", + "version": "v11.3.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench.git", + "reference": "7081895e5bbbdf2fd333fdf34e8ddec6cd4db713" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/7081895e5bbbdf2fd333fdf34e8ddec6cd4db713", + "reference": "7081895e5bbbdf2fd333fdf34e8ddec6cd4db713", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.23", + "laravel/framework": "^13.32.0", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^11.5.0", + "orchestra/workbench": "^11.3.0", + "php": "^8.3", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "symfony/process": "^7.4.5|^8.0.5", + "symfony/yaml": "^7.4|^8.0", + "vlucas/phpdotenv": "^5.6.1" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Laravel Testing Helper for Packages Development", + "homepage": "https://packages.tools/testbench/", + "keywords": [ + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench/tree/v11.3.0" + }, + "time": "2026-09-24T07:19:57+00:00" + }, + { + "name": "orchestra/testbench-core", + "version": "v11.6.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench-core.git", + "reference": "f139a4ee74638dc24e0b6f657b47226c8ba5ab05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/f139a4ee74638dc24e0b6f657b47226c8ba5ab05", + "reference": "f139a4ee74638dc24e0b6f657b47226c8ba5ab05", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "orchestra/sidekick": "~1.1.25|~1.2.22", + "php": "^8.3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-php84": "^1.34.0" + }, + "conflict": { + "brianium/paratest": "<7.3.0|>=8.0.0", + "laravel/framework": "<13.35.0|>=14.0.0", + "laravel/serializable-closure": ">=2.0.0 <2.0.10|>=3.0.0", + "nunomaduro/collision": "<8.9.0|>=9.0.0", + "phpunit/phpunit": "<11.5.50|>=12.0.0 <12.5.8|>=13.5.0" + }, + "require-dev": { + "fakerphp/faker": "^1.24", + "laravel/framework": "^13.35.0", + "laravel/pint": "^1.24", + "laravel/serializable-closure": "^2.0.10", + "mockery/mockery": "^1.6.10", + "phpstan/phpstan": "2.2.16", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "spatie/laravel-ray": "^1.43.6", + "symfony/process": "^7.4.5|^8.0.5", + "symfony/yaml": "^7.4.0|^8.0.0", + "vlucas/phpdotenv": "^5.6.1" + }, + "suggest": { + "brianium/paratest": "Allow using parallel testing (^7.3).", + "ext-pcntl": "Required to use all features of the console signal trapping.", + "fakerphp/faker": "Allow using Faker for testing (^1.23).", + "laravel/framework": "Required for testing (^13.35.0).", + "mockery/mockery": "Allow using Mockery for testing (^1.6).", + "nunomaduro/collision": "Allow using Laravel style tests output and parallel testing (^8.9).", + "orchestra/testbench-dusk": "Allow using Laravel Dusk for testing (^11.0).", + "phpunit/phpunit": "Allow using PHPUnit for testing (^11.5.50|^12.5.8|^13.0.0).", + "symfony/process": "Required to use Orchestra\\Testbench\\remote function (^7.4|^8.0).", + "symfony/yaml": "Required for Testbench CLI (^7.4|^8.0).", + "vlucas/phpdotenv": "Required for Testbench CLI (^5.6.1)." + }, + "bin": [ + "testbench" + ], + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Orchestra\\Testbench\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Testing Helper for Laravel Development", + "homepage": "https://packages.tools/testbench", + "keywords": [ + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench-core" + }, + "time": "2026-10-07T23:31:28+00:00" + }, + { + "name": "orchestra/workbench", + "version": "v11.3.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/workbench.git", + "reference": "660046639af4538d6c56373c9222ca4c93055c42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/workbench/zipball/660046639af4538d6c56373c9222ca4c93055c42", + "reference": "660046639af4538d6c56373c9222ca4c93055c42", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.23", + "laravel/framework": "^13.0.0", + "laravel/pail": "^1.2.5", + "laravel/tinker": "^3.0.0", + "nunomaduro/collision": "^8.9", + "orchestra/canvas": "^11.0.1", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "orchestra/testbench-core": "^11.5.0", + "php": "^8.3", + "symfony/process": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "require-dev": { + "laravel/pint": "^1.22.0", + "mockery/mockery": "^1.6.12", + "phpstan/phpstan": "^2.1.33", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "spatie/laravel-ray": "^1.43.6" + }, + "suggest": { + "ext-pcntl": "Required to use all features of the console signal trapping." + }, + "type": "library", + "autoload": { + "psr-4": { + "Orchestra\\Workbench\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Workbench Companion for Laravel Packages Development", + "keywords": [ + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/workbench/issues", + "source": "https://github.com/orchestral/workbench/tree/v11.3.0" + }, + "time": "2026-09-23T23:52:50+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.3.1", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6155239b488e95d26d1ebd95c6b4bb294d7f09db", + "reference": "6155239b488e95d26d1ebd95c6b4bb294d7f09db", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-10-08T21:09:11+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7440368d94738d54f2c1de24dbd3871793bbb0dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7440368d94738d54f2c1de24dbd3871793bbb0dd", + "reference": "7440368d94738d54f2c1de24dbd3871793bbb0dd", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.9.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0.1", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.37" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-10-03T14:03:50+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a248d1640ab059b075f53a2ef0f9856e864e06b5", + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.33" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-08-25T14:40:53+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "babab73b7ef9fae406f2eb3ee11323a61caa63d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/babab73b7ef9fae406f2eb3ee11323a61caa63d5", + "reference": "babab73b7ef9fae406f2eb3ee11323a61caa63d5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.37" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-10-03T04:51:29+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.40", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "76bd3198f16aeb375a4d5f3626471445aed0966e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/76bd3198f16aeb375a4d5f3626471445aed0966e", + "reference": "76bd3198f16aeb375a4d5f3626471445aed0966e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.8", + "phpunit/php-file-iterator": "^6.0.2", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.1", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", + "sebastian/diff": "^7.0.1", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.3", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.4", + "sebastian/version": "^6.0.1", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.40" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-10-16T05:55:07+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.24", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.24" + }, + "time": "2026-06-29T15:41:09+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-05-17T05:29:34+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "7c65c1e79836812819705b473a90c12399542485" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-05-21T04:45:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.33", + "symfony/process": "^7.4.17" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-08-25T15:35:54+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.26" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:40:20+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-05-20T04:37:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.5.28" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:10:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-05-19T16:22:07+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "82ff822c2edc46724be9f7411d3163021f602773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-05-20T06:45:45+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "9e935a16187e14f39b0e79910dabd614972cdb26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/9e935a16187e14f39b0e79910dabd614972cdb26", + "reference": "9e935a16187e14f39b0e79910dabd614972cdb26", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" + } + ], + "time": "2026-10-04T05:50:08+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symplify/easy-coding-standard", + "version": "11.5.0", + "source": { + "type": "git", + "url": "https://github.com/ecsphp/ecs.git", + "reference": "1d2400f7bfe92e3754ce71f0782f2c0521bade3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ecsphp/ecs/zipball/1d2400f7bfe92e3754ce71f0782f2c0521bade3d", + "reference": "1d2400f7bfe92e3754ce71f0782f2c0521bade3d", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "conflict": { + "friendsofphp/php-cs-fixer": "<3.0", + "squizlabs/php_codesniffer": "<3.6", + "symplify/coding-standard": "<11.3" + }, + "bin": [ + "bin/ecs" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Use Coding Standard with 0-knowledge of PHP-CS-Fixer and PHP_CodeSniffer", + "keywords": [ + "Code style", + "automation", + "fixer", + "static analysis" + ], + "support": { + "issues": "https://github.com/easy-coding-standard/ecs/issues", + "source": "https://github.com/easy-coding-standard/ecs/tree/11.5.0" + }, + "funding": [ + { + "url": "https://www.paypal.me/rectorphp", + "type": "custom" + }, + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2023-06-21T06:26:15+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/tests/Performance/results/issue-748-20261018/OBSERVER-OVERHEAD.md b/tests/Performance/results/issue-748-20261018/OBSERVER-OVERHEAD.md new file mode 100644 index 000000000..0e2f981d9 --- /dev/null +++ b/tests/Performance/results/issue-748-20261018/OBSERVER-OVERHEAD.md @@ -0,0 +1,158 @@ +# Embedded continuation observer comparison + +## Scope + +This separately declared study measures the optional listeners in the growing-history +probe for [#748](https://github.com/durable-workflow/workflow/issues/748). Its source +is `9ad7d9b14a74e1755c2c377989b3e2e324375301`, tree +`9c906771dbf84c5e603ac22617e11fe25d5e2d3c`, including released main `c301f442`. +The earlier measurements and unsuccessful one-hour pilot retain their original +source and outcomes. Experimental retention remains disabled by default. + +## Method + +Each cell creates a fresh container, SQLite database and persistent Testbench queue +worker. One workflow runs 19 activity/timer rounds and a final timer, giving +100 events, 19 unique effects and 79 completed durable tasks. Timers wait one real +second. The probe verifies typed bigint/null/Unicode values, contiguous history, +the original admission prefix, recorded workflow clocks and original deadlines, +zero repairs/failed jobs, an empty queue and ordinary worker exit zero. + +Replay and retained modes change only the existing allowlist. Retention keeps its +four-entry, 30-second nonrenewing age and 128 MiB worker-RSS thresholds. Actual cache +hits, misses and authored entries are reported. No affinity or hit is forced. + +Full observation enables the existing SQL, task-claim delay, timer-lateness and +cache-peak listeners. Minimal observation disables those listeners while retaining +the committed-completion barrier, authored-entry counters, process CPU/memory, +source/lock checks and final audit. Its unavailable optional fields remain null. +It is still a source probe with Testbench, rather than an uninstrumented Artisan +application measurement. + +Three matched pairs per execution mode use the order declared before admission. +Repetition1 runs replay/full, replay/minimal, retained/full, retained/minimal. +Repetition2 reverses that order. Repetition3 runs replay/minimal, replay/full, +retained/minimal, retained/full. A final fresh replay/minimal cell checks drift. +Every original keeps 3,600-second run/execution/observation bounds, 30-second worker +startup and 15-second queue/worker drain. Failed originals are not renewed. + +The isolated virtual host reports two logical CPUs, Intel Xeon Skylake (IBRS), +full virtualization with hypervisor vendor Microsoft, Linux 6.8.0-146 and +Docker 29.1.3 with overlayfs/cgroupv2. Each measured container has one CPU, +1 GiB aggregate memory and zero swap. NTP synchronization and the kernel aggregate +peak counter were checked before admission. Build/dependency preparation and four +successful functional controls finished first. No competing application/build +container runs during the study. Host steal observations are reported separately. + +The PHP tool image is +`docker.io/durableworkflow/server@sha256:cba7c4c6732529293ef6c2625a64785dcb21b4f7fe0013fdc74f1af466f534ac`, +amd64 child `8248d5863b2bd296a0635af5deb181046ee86391f460a331a18f44efa625cdd7`, +config `4199bcbaf349a600cbb29a7ced6170b72c49a51828c39f0e7963dc102b642911`. +Its inherited healthcheck is disabled. PHP 8.3.35, Laravel 13.37.0, +Testbench 11.3.0 and PHPUnit 12.5.40 use one normal dependency installation with +Composer root version 2.6.1. The exact generated input lock is provided beside this report, +SHA-256 `d8d433d6e5e4e4a6d5a9c10a85608b44495636175f89fd04e811bbaf7601cfef`. +All seven loaded runtime/fixture hashes and this lock must match in every worker. + +Worker queue CPU excludes its separately recorded cold Testbench startup. Aggregate +workload CPU includes the controller and worker through queue and worker drain, +ending before final audit queries. Aggregate peak memory covers the entire fresh +container, including startup and audit. Per-process RSS peaks are not summed. + +## Observations + +All thirteen originals pass. The thirteen cells ran from +**12:56:12.652 UTC to 13:14:49.984 UTC on October 18, 2026**. Every cell has its +own original run and container. The source/lock checks, resource limits, recorded +clocks, history/effect/task audits and teardown checks pass. Across the study: +**13 workflows, 1,300 events, 247 unique effects and 1,027 completed durable tasks**, +with zero repairs, failed jobs or unfinished work. All workers and containers exit +zero, with no OOM or restart. + +| Cell | Worker CPU | Aggregate CPU | Workload wall | Peak MiB | Authored entries | Cache hits / misses | +| --- | ---: | ---: | ---: | ---: | ---: | --- | +| r1-replay-full | 51.423 | 51.893 | 87.637 | 176.48 | 40 | disabled | +| r1-replay-minimal | 51.715 | 52.191 | 88.808 | 176.96 | 40 | disabled | +| r1-retained-full | 42.223 | 42.653 | 79.298 | 176.70 | 3 | 37 / 3 | +| r1-retained-minimal | 42.769 | 43.193 | 76.930 | 176.74 | 3 | 37 / 3 | +| r2-retained-minimal | 42.447 | 42.859 | 76.668 | 176.78 | 3 | 37 / 3 | +| r2-retained-full | 42.717 | 43.135 | 76.771 | 177.00 | 3 | 37 / 3 | +| r2-replay-minimal | 51.964 | 52.440 | 87.037 | 176.51 | 40 | disabled | +| r2-replay-full | 50.594 | 51.044 | 84.670 | 176.52 | 40 | disabled | +| r3-replay-minimal | 50.066 | 50.610 | 101.341 | 176.63 | 40 | disabled | +| r3-replay-full | 49.577 | 50.021 | 83.111 | 176.85 | 40 | disabled | +| r3-retained-minimal | 44.308 | 44.713 | 75.872 | 177.02 | 3 | 37 / 3 | +| r3-retained-full | 43.715 | 44.123 | 75.841 | 176.96 | 3 | 37 / 3 | +| drift-replay-minimal | 49.446 | 49.877 | 82.444 | 176.63 | 40 | disabled | + +CPU and wall values are seconds; peak memory is the whole-container MiB peak. +Worker startup CPU is separate and ranges from 0.579 to 0.623 seconds. All six +retained cells have three authored entries, 37 cache hits and three misses. Each +has two age evictions/discards in total; expiry placement changes the individual +`evict_age`/`discard_age` counters without changing the hit/miss totals. Replay +cells enter authored code 40 times. No observation is rescaled or noise-corrected. + +| Mode | Full CPU median | Minimal CPU median | Median paired change | Paired range | +| --- | ---: | ---: | ---: | --- | +| replay | 50.594 | 51.715 | -0.98% | -2.64% to -0.56% | +| retained | 42.717 | 42.769 | -1.28% | -1.34% to +0.64% | + +Paired change uses `(full / minimal - 1) * 100` for matching repetition numbers. +The final replay/minimal drift control uses **49.445904 worker CPU seconds** +versus the initial **51.714887**, a change of **-4.39%**. Per-cell observed host +steal ranges from 0% to 0.00636%; its cause and the CPU drift are not attributed. + +The full-observer cells each contain 79 task-claim-delay observations and 20 timer +lateness observations. Their per-cell p95 values and SQL counts are: + +| Full-observer cell | SQL count | History SELECTs | SQL elapsed ms | Claim p95 ms | Timer lateness p95 ms | +| --- | ---: | ---: | ---: | ---: | ---: | +| r1-replay-full | 11298 | 472 | 2916.430 | 1513.439 | 1897.584 | +| r1-retained-full | 11380 | 551 | 2822.430 | 1442.920 | 2010.657 | +| r2-retained-full | 11352 | 551 | 2848.690 | 1397.030 | 1789.932 | +| r2-replay-full | 11294 | 472 | 2863.760 | 1347.265 | 1755.450 | +| r3-replay-full | 11286 | 472 | 2863.240 | 1386.273 | 1811.595 | +| r3-retained-full | 11332 | 551 | 2917.340 | 1507.603 | 1795.700 | + +SQL elapsed time is the worker's cumulative reported query duration, not database +CPU. Minimal cells report all these optional fields as null. Retained cells make +551 history SELECTs versus replay's 472 in this workload; no database-read +reduction is established. + +## Interpretation and remaining acceptance + +The six paired worker-CPU changes range from **-2.64% to +0.64%**, smaller than +the observed **4.39%** baseline drift. This study does not resolve a positive +listener cost. Negative estimates do not establish a benefit from observation, +and no estimated observer cost is subtracted from earlier results. + +On this specific source workload, retained and replay worker-CPU medians remain +separated under both observation settings. The full-observer medians are +42.717 and 50.594 seconds; minimal medians are 42.769 and 51.715 seconds. +Every retained cell avoids 37 completed-prefix re-entries while preserving the +original outcome. These are descriptive results from three repetitions, with +material baseline drift; they do not establish a production saving. + +The next runtime investigation is repeated history-payload decoding already +identified by `BridgePayloadProbeTest`. A candidate must preserve raw bytes, +mutation/invalidation, serialization and configured-model behavior. Source +correctness and a new declared comparison are required before choosing it or +admitting another larger-history measurement. This result does not justify +relaxing correctness diagnostics or extending the failed one-hour pilot. + +This study covers one source workflow size and one worker/run topology. The +remaining 1,000-event, concurrency, hit/miss/pressure, lifetime and published-artifact +requirements of #748 stay open. It does not qualify service-mode warm execution. + +## Reproduce + +Use source `9ad7d9b1` and copy the adjacent `COMPOSER-INPUT.lock` to its root as +`composer.lock`. Install with `COMPOSER_ROOT_VERSION=2.6.1` in the pinned PHP image, +then keep the source, vendor tree and lock unchanged throughout the comparison. +Use the [existing recipe](../../EMBEDDED-CONTINUATION.md) and one fresh constrained +container per cell, selecting `100-events_1-workers_1-runs_MODE_repeat-N`. +Set `DWF_GROWTH_OBSERVERS=on` or `off` for its declared cell, with +`PROFILE_EMBEDDED_CONTINUATION=measure` and `XDEBUG_MODE=off`. +The drift cell selects a separate fresh replay/minimal repeat1 original. The +experiment host, task access key, source/dependency copies and raw samples are +retired after immediate audit. The input lock and this report remain reproducible. diff --git a/tests/Performance/results/issue-748-20261020/CONCURRENCY-100.md b/tests/Performance/results/issue-748-20261020/CONCURRENCY-100.md new file mode 100644 index 000000000..b1dc0b852 --- /dev/null +++ b/tests/Performance/results/issue-748-20261020/CONCURRENCY-100.md @@ -0,0 +1,45 @@ +# 100-event concurrent embedded execution, October 20, 2026 + +These are original observations on source `ae29003f8308869c3aca84dd0c0ce61b3e0eb31e`, tree `14dffaf0485a0795c85dc78297682825ed8762b8`. They predate the claimed-attempt correction and revised maintenance audit; they do not qualify those later changes or a release. The [small comparison](GROWTH.md) describes the workload and [10-event concurrency report](CONCURRENCY.md) covers the smaller cohorts. + +Each mode uses a fresh container with one CPU, 1 GiB aggregate memory and no swap, the same locked dependencies and original 3,600-second workflow/observation budget, 30-second startup and 15-second drain bounds. PHP 8.5.6, Laravel 13.38.0, SQLite 3.51.2; input lock SHA-256 `0e41dd1ce1043d7ab1493a9916f219c571a9b931cc4f448d0b73da2b535eef8d`. Optional observers are enabled. Real database queues, activities and one-second timers are used. + +Of eighteen original cohorts, seventeen pass all of their then-current audit gates (94 workflows). One eight-workflow replay cohort fails its zero-repair gate after all workflows completed; the later semantic gates did not run for that original. It remains failed. No cohort was resumed or rebudgeted. No container was OOM-killed or restarted. + +Wall and CPU are seconds; peak is whole-container MiB. CPU is aggregate queue-worker CPU, including history/projection, hydration, dispatch, activity and observer work. It is not isolated replay or authored-code CPU. Host/image facts are per pair; host variation prevents pooling these as one production speedup estimate. + +| Workers / runs | Repetition | Replay wall / CPU | Retained wall / CPU | Peak replay / retained MiB | Retained hits / misses | Retained cold entries | +|---|---:|---:|---:|---:|---:|---:| +| 1 / 8 | 1 | 153.127 / 136.867 | 156.291 / 142.164 | 163.72 / 165.04 | 4 / 316 | 316 | +| 1 / 8 | 2 | 144.860 / 133.868 | 149.935 / 133.932 | 169.69 / 169.62 | 5 / 315 | 315 | +| 1 / 8 | 3 | 148.988 / 96.109 | 141.525 / 96.241 | 164.61 / 164.91 | 20 / 300 | 300 | +| 4 / 1 | 1 | 47.546 / 14.857 | 47.889 / 13.974 | 329.47 / 328.14 | 34 / 6 | 6 | +| 4 / 1 | 2 | 51.194 / 18.207 | 51.215 / 16.863 | 341.84 / 340.36 | 32 / 8 | 8 | +| 4 / 1 | 3 | 47.410 / 9.631 | 47.440 / 9.177 | 338.35 / 340.92 | 33 / 7 | 7 | +| 4 / 8 | 1 | failed audit; no complete report | 148.638 / 138.698 | unreported / 400.22 | 64 / 256 | 256 | +| 4 / 8 | 2 | 133.584 / 121.412 | 131.942 / 119.423 | 389.56 / 391.18 | 68 / 252 | 252 | +| 4 / 8 | 3 | 111.692 / 93.936 | 108.828 / 94.428 | 389.29 / 389.83 | 73 / 247 | 247 | + +The one-worker/eight-run retained cohorts are miss-heavy: 4–20 hits versus 300–316 misses, with 292–308 capacity evictions. Their worker CPU does not decrease. Four workers with one run obtain 32–34 hits, but still do 512 history SELECTs per workflow versus replay's 433. Four workers with eight runs obtain 64–73 hits versus 247–256 misses. There is no affinity wait or cache-threshold change. All successful eight-run cells make 4,096 retained versus 3,464 replay history SELECTs. Avoiding authored re-entry does not remove authoritative history/projection work. + +## Failed original + +[Run 39455106192](https://github.com/durable-workflow/workflow/actions/runs/39455106192), replay repetition 1, completed eight workflows, 800 history events, 152 activity effects and 632 tasks, with no failed jobs and an empty transport queue. One activity task had `repair_count=1`, `attempt_count=1`. Its available time was `16:16:46.903153`, last dispatch `16:16:47.008060`, and first lease `16:16:53.232293` UTC. The audit failed at the original zero-repair assertion; this is not a complete semantic pass. + +Source review shows the watchdog can redispatch a healthy Ready task after its ordinary stale-dispatch threshold when no unreserved broker delivery remains. A delivery can be reserved before its logical claim. This is consistent with the diagnostic timing; it is not a reproduction of a lost effect, expired execution lease or customer defect. Fresh controls on `70ed2c27` reserve a real database-queue delivery across the threshold, then require one logical attempt, one business effect and acknowledgement of the obsolete delivery. Separate entry-repair and killed-worker controls classify actual error repair and expired-lease recovery. The revised future audit reconciles every persisted repair-count increment with these observations, rejects lease/error recovery and still requires original IDs/history/deadlines and first-attempt tasks. It does not change the old result or product timing policy. + +## Exact observation receipts + +| Workers / runs | Repetition | CPU model | Built image | +|---|---:|---|---| +| 1 / 8 | 1 | AMD EPYC 7763 64-Core Processor | `sha256:1606a56e22bfd271600d64ebdf8dd3c915384cba4185de0244f673d632951335` | +| 1 / 8 | 2 | AMD EPYC 7763 64-Core Processor | `sha256:a03c5e2f1fce1ef5531cac19f323f0bc040ec46e1c0de581d036f1a10da94de2` | +| 1 / 8 | 3 | AMD EPYC 9V74 80-Core Processor | `sha256:3f42585f85b8d1dfebdf0583f139aa872fe8435ef32391cbba86efc92c611a97` | +| 4 / 1 | 1 | Intel(R) Xeon(R) 6973P-C | `sha256:923f8a5dbe12c94accca00feedd5b4a8795de9f747a5c09492295ff570d7ebd5` | +| 4 / 1 | 2 | AMD EPYC 7763 64-Core Processor | `sha256:02fe7d283ca4965ab57b73d11243af66c27aaa777ccfd988de2780fa5f771074` | +| 4 / 1 | 3 | AMD EPYC 9V45 96-Core Processor | `sha256:76139b076d479a7211ea5f47cc0a53c9e031333d04ad0a52e434b7777d5f2ac6` | +| 4 / 8 | 1 | AMD EPYC 7763 64-Core Processor | `sha256:9af36305b63ea727ac70cf853434432fe1004e62336323b75d787f5513f454bc` | +| 4 / 8 | 2 | INTEL(R) XEON(R) PLATINUM 8573C | `sha256:9aa95ba937f51a1fcb24311043ca79a9e9f3fd76123a76d430f781badcf7c499` | +| 4 / 8 | 3 | Intel(R) Xeon(R) 6973P-C | `sha256:27f55debbbc65cd8c615c57194848f565c16d547310b1aa31b1f9b9e6bd254fd` | + +Actions retain scalar reports, source/image/lock checks, startup CPU, query elapsed time, task-claim delay and timer-lateness distributions: [1 workers / 8 runs](https://github.com/durable-workflow/workflow/actions/runs/39454539611), [4 workers / 1 runs](https://github.com/durable-workflow/workflow/actions/runs/39455101819), [4 workers / 8 runs](https://github.com/durable-workflow/workflow/actions/runs/39455106192). No database/history archive is published. diff --git a/tests/Performance/results/issue-748-20261020/CONCURRENCY.md b/tests/Performance/results/issue-748-20261020/CONCURRENCY.md new file mode 100644 index 000000000..b9d34e784 --- /dev/null +++ b/tests/Performance/results/issue-748-20261020/CONCURRENCY.md @@ -0,0 +1,44 @@ +# Small concurrent and multiworker originals + +This study covers eighteen fresh 10-event cohorts on source `ae29003f8308869c3aca84dd0c0ce61b3e0eb31e`, tree `14dffaf0485a0795c85dc78297682825ed8762b8`. Runtime equals database-session-fenced `9a4e0e9e`. Its input lock is SHA-256 `0e41dd1ce1043d7ab1493a9916f219c571a9b931cc4f448d0b73da2b535eef8d`. PHP is 8.5.6, Laravel 13.38.0, SQLite 3.51.2 and Avro 1.12.2. + +All eighteen cohorts pass, with **102 unique original runs, 1,020 events, 102 unique activity effects, 714 completed tasks and 204 timers**, zero repairs/failed jobs/errors/unfinished work. Exact loaded source/lock, immutable prefixes/deadlines, typed results, clocks and empty-queue/worker-exit controls pass. Every container exits zero without OOM or restart. + +Each original cohort uses its own container/database, one CPU / 1 GiB aggregate memory / no swap, actual database jobs and one-second timers. The aggregate includes the controller and all four workers in the multiworker cells. Workers retain four entries each, a nonrenewing 30-second age and the unchanged 128 MiB worker-RSS threshold. No affinity or cache hit is forced. Repetition two reverses mode order. Each pair shares one runner/image; hosted CPU/image identities vary between pairs and remain in the Actions logs. + +[One worker / eight runs](https://github.com/durable-workflow/workflow/actions/runs/39453372320), [four workers / one run](https://github.com/durable-workflow/workflow/actions/runs/39453827994) and [four workers / eight runs](https://github.com/durable-workflow/workflow/actions/runs/39453831580) provide all reports, host/image identities and cleanup results. These three topologies do not replace the separately attributed one-worker/one-run study or larger-history acceptance. + +## Outcomes + +CPU sums actual queue CPU across the worker processes; startup CPU is separate. Peak MiB is the kernel whole-container peak, including startup/audit. Per-process RSS peaks are not added. History SELECTs are cohort totals. + +| Workers | Runs | Rep | Mode | Worker CPU s | Queue wall s | Aggregate peak MiB | Entries | Hits / misses | History SELECTs | +| ---: | ---: | ---: | --- | ---: | ---: | ---: | ---: | --- | ---: | +| 1 | 8 | 1 | replay | 3.606557 | 9.943969 | 138.22 | 32 | disabled | 296 | +| 1 | 8 | 1 | retained | 3.862739 | 9.241938 | 138.86 | 28 | 4 / 28 | 352 | +| 1 | 8 | 2 | replay | 2.671366 | 9.296034 | 138.39 | 32 | disabled | 296 | +| 1 | 8 | 2 | retained | 2.764094 | 9.406500 | 138.90 | 28 | 4 / 28 | 352 | +| 1 | 8 | 3 | replay | 3.485510 | 9.844621 | 143.72 | 32 | disabled | 296 | +| 1 | 8 | 3 | retained | 3.527411 | 9.893154 | 144.04 | 28 | 4 / 28 | 352 | +| 4 | 1 | 1 | replay | 1.034570 | 7.086144 | 294.52 | 4 | disabled | 37 | +| 4 | 1 | 1 | retained | 1.161962 | 6.736168 | 306.86 | 3 | 1 / 3 | 44 | +| 4 | 1 | 2 | replay | 0.888284 | 6.560303 | 293.94 | 4 | disabled | 37 | +| 4 | 1 | 2 | retained | 0.896661 | 6.590698 | 293.16 | 3 | 1 / 3 | 44 | +| 4 | 1 | 3 | replay | 0.889351 | 8.744812 | 286.93 | 4 | disabled | 37 | +| 4 | 1 | 3 | retained | 0.934561 | 6.733956 | 300.08 | 4 | 0 / 4 | 44 | +| 4 | 8 | 1 | replay | 4.310177 | 9.579241 | 302.79 | 32 | disabled | 296 | +| 4 | 8 | 1 | retained | 4.403000 | 10.776011 | 308.62 | 28 | 4 / 28 | 352 | +| 4 | 8 | 2 | replay | 3.904036 | 8.587625 | 293.95 | 32 | disabled | 296 | +| 4 | 8 | 2 | retained | 4.107442 | 10.556261 | 302.62 | 31 | 1 / 31 | 352 | +| 4 | 8 | 3 | replay | 3.034582 | 10.238906 | 313.70 | 32 | disabled | 296 | +| 4 | 8 | 3 | retained | 3.121324 | 10.709189 | 311.64 | 27 | 5 / 27 | 352 | + +The eight-run, one-worker originals expose actual capacity misses: each has four hits, 28 misses and 20 capacity evictions. Four-worker/single-run originals have zero or one hit; handoff selects cold replay. Four-worker/eight-run originals have one to five hits with 11–15 capacity evictions in total. All logical outcomes remain equivalent and every original completes within its original budget. + +Retention uses more worker CPU in every small concurrent/multiworker pair here, with small and variable wall differences. At ten events, completed-prefix savings do not offset cache validation and ordinary misses in these topologies. This supports opt-in deployment chosen from actual reuse measurements; it does not support enabling retention by default or a capacity/speedup claim. All hit/miss counts are observed rather than selected by a forced routing policy. + +## Remaining work + +The 100/1,000-event concurrent cases, repeated large originals, replay-phase attribution, tracing integration, final source/regression/coverage and published package/consumer qualification remain open. Earlier failures remain failed. These samples qualify their exact declared source and conditions; no whole-tree or performance acceptance transfers to a later candidate. + +Raw histories and effect rows are audited in place. Disposable containers, databases and dependencies are removed by the workflow, with no history/database archive uploaded. The readable report and exact source/lock references remain. diff --git a/tests/Performance/results/issue-748-20261020/CURRENT-SOURCE.md b/tests/Performance/results/issue-748-20261020/CURRENT-SOURCE.md new file mode 100644 index 000000000..3d2412079 --- /dev/null +++ b/tests/Performance/results/issue-748-20261020/CURRENT-SOURCE.md @@ -0,0 +1,69 @@ +# Later source comparisons, October 20, 2026 + +These fresh comparisons qualify their exact source only. Each mode uses a fresh one-CPU / 1 GiB aggregate / no-swap container, real database queues and one-second durable timers, with the original 3,600-second workflow/execution/observation budget, 30-second startup and 15-second drain. PHP 8.5.6, Laravel 13.38.0 and SQLite 3.51.2 use input lock SHA-256 `0e41dd1ce1043d7ab1493a9916f219c571a9b931cc4f448d0b73da2b535eef8d`. Observers are enabled. Each pair shares its host/image/dependency lock; pair order alternates. Hardware and drift prevent pooling the pairs into one production speedup estimate. + +CPU is aggregate queue-worker process CPU, including projection/history, dispatch, activities and instrumentation. It is not isolated authored or completed-prefix CPU. Peaks cover the whole container. Process startup, task-claim delay, timer lateness and SQL elapsed remain separate in the scalar Actions records. No history/database archive is published. The [large concurrent failures](GROWTH-1000.md) and all earlier failed originals remain failed. + +## 10 events, one worker, one run + +[Run 39478490070](https://github.com/durable-workflow/workflow/actions/runs/39478490070), source `fa5d23009245b0c975a685956131ae1e575e49a3`, passes all six independent original cohorts. Each cohort completes its one workflow. Every original passes its full effect/history/clock/deadline/first-attempt/empty-transport/drain audit, with zero errors, unfinished runs, repairs, healthy redispatches, OOM kills or restarts. + +| Repetition | Replay wall / CPU seconds | Retained wall / CPU seconds | Peak replay / retained MiB | Retained hits / misses | Retained cold entries | +|---|---:|---:|---:|---:|---:| +| 1 | 7.256 / 0.457 | 7.679 / 0.460 | 133.28 / 133.56 | 3 / 1 | 1 | +| 2 | 6.679 / 0.570 | 6.680 / 0.573 | 133.10 / 133.15 | 3 / 1 | 1 | +| 3 | 6.424 / 0.327 | 6.414 / 0.321 | 133.11 / 133.55 | 3 / 1 | 1 | + +Retained execution enters authored PHP once instead of four times and makes 44 history SELECTs versus 37. The tiny CPU differences do not establish a production saving. + +| Repetition | CPU model | Built image | +|---|---|---| +| 1 | AMD EPYC 9V74 80-Core Processor | `sha256:51716d016610b184ce9673012d5d812305e3eedbd12b5f0d2462ca81b816b4ed` | +| 2 | AMD EPYC 9V74 80-Core Processor | `sha256:18afc1540745a3f22cb516fb59b983ed77574b0b4ce6335e3ef8b727f1d01268` | +| 3 | AMD EPYC 9V45 96-Core Processor | `sha256:0987209fabe8d61c237696b2aea90d87ac3718a0de436362c6d7d61a24b73c7b` | + +## 100 events, four workers, eight runs + +[Run 39476212925](https://github.com/durable-workflow/workflow/actions/runs/39476212925), source `5dd594e397184ee1f22a7a5dfaef867e70c530b6`, passes all six independent original cohorts. All 48 workflows complete across the six cohorts. Every original passes its full effect/history/clock/deadline/first-attempt/empty-transport/drain audit, with zero errors, unfinished runs, repairs, healthy redispatches, OOM kills or restarts. + +| Repetition | Replay wall / CPU seconds | Retained wall / CPU seconds | Peak replay / retained MiB | Retained hits / misses | Retained cold entries | +|---|---:|---:|---:|---:|---:| +| 1 | 121.294 / 96.579 | 121.409 / 97.193 | 401.11 / 401.69 | 74 / 246 | 246 | +| 2 | 160.208 / 149.864 | 154.898 / 145.200 | 389.95 / 389.99 | 79 / 241 | 241 | +| 3 | 112.237 / 100.527 | 116.635 / 99.882 | 391.10 / 393.28 | 73 / 247 | 247 | + +Replay enters authored PHP 320 times per cohort; retained execution enters 246 / 241 / 247 times. The four-entry worker caches remain miss-heavy. History SELECTs increase from 3,464 to 4,096 per cohort. There is no consistent worker-CPU saving or affinity assumption. This source predates the later broad-signature admission correction; the measured integer entry is unchanged by that correction. + +| Repetition | CPU model | Built image | +|---|---|---| +| 1 | AMD EPYC 9V74 80-Core Processor | `sha256:0302c1819f88703e067648980ebc5352bd8b8ef4b686a0b5d44b0e5eca9e6ccf` | +| 2 | AMD EPYC 7763 64-Core Processor | `sha256:5f589f8fe959ce2d026f8cce74965d87f18efbda5e22a2ae784f05e13147d1d4` | +| 3 | Intel(R) Xeon(R) 6973P-C | `sha256:9defa74970d3d68592fa66238cd23f525789fc16008cafa37300259db10e1020` | + +## 1,000 events, one worker, one run + +[Run 39476208605](https://github.com/durable-workflow/workflow/actions/runs/39476208605), source `5dd594e397184ee1f22a7a5dfaef867e70c530b6`, passes all three pairs / six originals. Each cohort completes its workflow with 1,000 contiguous events, 199 typed activity effects, 799 first-attempt tasks and 200 durable timers. Full audits verify the admission prefix, recorded clocks/results, original deadlines, empty transport and graceful drain. There are zero errors, unfinished runs, task repairs, healthy redispatches, OOM kills or restarts. + +| Repetition | Replay wall / CPU seconds | Retained wall / CPU seconds | Peak replay / retained MiB | Retained hits / misses | Retained cold entries | +|---|---:|---:|---:|---:|---:| +| 1 | 2820.913 / 2636.072 | 1636.895 / 1458.272 | 193.34 / 193.93 | 356 / 44 | 44 | +| 2 | 2730.400 / 2537.499 | 1584.593 / 1404.618 | 198.80 / 199.65 | 357 / 43 | 43 | +| 3 | 2946.513 / 2767.292 | 1687.810 / 1518.125 | 193.30 / 194.78 | 355 / 45 | 45 | + +Replay enters authored PHP 400 times / 40,198 round visits per cohort. Retained execution enters 44 / 43 / 45 times, with 5,544 / 5,369 / 5,647 visits. History SELECTs increase from 4,393 to 5,192 per run; each retained cohort validates 200,399 committed history events. Frames retain their four-entry / nonrenewing 30-second / 128 MiB worker-RSS bounds, and no entries remain after drain. Age misses occur naturally; no cache hit, queue affinity or worker lifetime is forced. + +The exclusive CPU breakdown separates repeated durable dispatch from authored PHP. Values are seconds; authored PHP includes forward execution as well as replay, and suspended waits are outside its brackets. + +| Repetition | History/projection replay / retained | Durable dispatch replay / retained | Validation replay / retained | Authored PHP replay / retained | +|---|---:|---:|---:|---:| +| 1 | 1164.840 / 1152.885 | 1356.439 / 225.512 | 51.391 / 8.437 | 4.676 / 0.618 | +| 2 | 1132.497 / 1122.816 | 1294.722 / 205.329 | 48.509 / 7.547 | 4.867 / 0.644 | +| 3 | 1208.623 / 1196.243 | 1435.201 / 237.169 | 55.296 / 8.941 | 6.050 / 0.843 | + +As in the earlier successful study, repeated dispatch and validation account for most of the observed CPU difference. History/projection remains substantial and authoritative. Whole-worker differences support bounded continuation for this workload; they do not isolate completed-prefix CPU or establish a production saving/capacity envelope. The failed large concurrent originals remain failed. This source predates the later broad-signature admission correction; its measured integer entry remains eligible under that correction. Functional-source correctness and normal published-package acceptance are separately qualified. + +| Repetition | CPU model | Built image | +|---|---|---| +| 1 | AMD EPYC 7763 64-Core Processor | `sha256:a4747658e09cb298a0005c3f555e0ef16ecef252ca90f27239054a6671435575` | +| 2 | AMD EPYC 7763 64-Core Processor | `sha256:66f98f5db02ae64c755a379163c61b29fc215853fd57d651d96e70a4ef6666de` | +| 3 | AMD EPYC 7763 64-Core Processor | `sha256:3af1041ef1a47d198ea725f8557ae6f44c9924c80d1e94664a62ace32dc47a49` | diff --git a/tests/Performance/results/issue-748-20261020/EXECUTOR-CPU.md b/tests/Performance/results/issue-748-20261020/EXECUTOR-CPU.md new file mode 100644 index 000000000..54688a0ca --- /dev/null +++ b/tests/Performance/results/issue-748-20261020/EXECUTOR-CPU.md @@ -0,0 +1,47 @@ +# Executor CPU observer and drift controls, October 20, 2026 + +[Run 39462885370](https://github.com/durable-workflow/workflow/actions/runs/39462885370) passes eighteen fresh 100-event originals on source `92bb602fb7479835ad4e4b7ecd67c5a01de80542`. This precedes the configured-model identity correction in `9ee8d641`; it is not final-source or published-package qualification. Each repetition keeps one host, image and lock across three observer blocks, with both replay modes in a fresh container in each block. Orders are on/off/on for repetitions 1 and 3, off/on/off for repetition 2. Replay/retained order is also reversed in repetition 2. No original is resumed or rebudgeted. + +All eighteen pass exact source/lock and ordinary workload audits: 1,800 total history events, 342 activity effects, original identities/prefixes/deadlines, recorded clocks/typed outputs, first-attempt tasks, zero task-maintenance transitions, no failed jobs/unfinished work, empty queues and successful worker drain. One CPU / 1 GiB aggregate memory / no swap and the original per-mode 3,600-second bounds remain unchanged. No container is OOM-killed or restarted. PHP 8.5.6, Laravel 13.38.0, SQLite 3.51.2; input lock SHA-256 `0e41dd1ce1043d7ab1493a9916f219c571a9b931cc4f448d0b73da2b535eef8d`. + +## Observer cost and measured drift + +CPU values are aggregate queue-worker seconds. The on/off estimate compares the central block with the mean of its two repeated-setting bookends, within the same host and replay mode. Drift is `(last repeated CPU / first repeated CPU - 1) * 100`. The estimate is always expressed as observer-on relative to observer-off. + +| Repetition | Mode | Observer order | CPU block 1 / 2 / 3 seconds | On vs off estimate | Repeated-setting drift | +|---|---|---|---:|---:|---:| +| 1 | replay | on / off / on | 12.051007 / 12.101955 / 12.882718 | +3.02% | +6.90% | +| 1 | retained | on / off / on | 11.258804 / 11.650038 / 11.971524 | -0.30% | +6.33% | +| 2 | replay | off / on / off | 13.236511 / 13.104947 / 13.015287 | -0.16% | -1.67% | +| 2 | retained | off / on / off | 12.424036 / 12.250443 / 12.183669 | -0.43% | -1.93% | +| 3 | replay | on / off / on | 13.613863 / 13.459163 / 13.654846 | +1.30% | +0.30% | +| 3 | retained | on / off / on | 12.707790 / 12.549586 / 12.674747 | +1.13% | -0.26% | + +The third host observes a roughly 1.1–1.3% on/off CPU increase, larger than its roughly 0.3% repeated-setting drift. The other hosts include negative estimates and drift up to 6.9%. These observations do not establish one portable overhead percentage or correction factor. Optional clock/listener overhead remains in all observer-on phase values. The off control still retains source/lock checks, business audits, maintenance and authoring counters, normal cache diagnostics and process CPU/memory readings; it is not an uninstrumented production baseline. + +The separately dispatched source-`6f087ee6` observer-on study [39461324018](https://github.com/durable-workflow/workflow/actions/runs/39461324018) also passes six originals. Its observer-off companion39461327140 was canceled while all jobs had zero steps, before any original ran. Different-host on/off jobs are not treated as paired observer-overhead evidence. Older reports and rejected observations are unchanged. + +## Actual phase attribution + +The following deterministic selection uses the first observer-on block in each repetition (block 1, 2, 1). CPU is seconds. All phase columns are exclusive of nested observed phases. Authoring sums actual Fiber start/resume/throw CPU between execution and suspension/return/throw; it includes workflow API calls and forward execution as well as replay. It is not isolated completed-prefix CPU. History/projection includes that role's SQL/history work. Initial loading does not represent every read in the task. Remaining CPU includes unclassified worker, claim/transaction, activity/transport and observer work. Inclusive buckets in the raw report overlap and must not be added together. + +| Repetition / mode | Worker | History / projection | Initial load | Initialization | Step validation | Durable dispatch | Authoring | Remaining | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| 1 / replay | 12.051007 | 9.255817 | 0.145473 | 0.016947 | 0.047312 | 1.289439 | 0.025453 | 1.270566 | +| 1 / retained | 11.258804 | 9.254205 | 0.149825 | 0.077162 | 0.008702 | 0.424107 | 0.002992 | 1.341811 | +| 2 / replay | 13.104947 | 10.068025 | 0.154315 | 0.017848 | 0.052924 | 1.423422 | 0.027538 | 1.360875 | +| 2 / retained | 12.250443 | 10.106109 | 0.158573 | 0.081560 | 0.010088 | 0.463752 | 0.003220 | 1.427141 | +| 3 / replay | 13.613863 | 10.455567 | 0.162167 | 0.018263 | 0.054814 | 1.485308 | 0.028428 | 1.409316 | +| 3 / retained | 12.707790 | 10.471219 | 0.163966 | 0.085761 | 0.010386 | 0.478679 | 0.003387 | 1.494392 | + +In these small-history cells, history/projection remains the largest measured CPU component. Retention avoids completed-prefix starts and some recorded-outcome dispatch/validation work, while adding cache/history validation. The authored PHP portion is small; do not describe the whole worker-CPU difference as authored-code CPU. The [1,000-event pilot](GROWTH-1000-PILOT.md) motivates repeated larger-history qualification but does not replace this phase measurement. + +## Exact per-host receipts + +| Repetition | CPU model | Built image | +|---|---|---| +| 1 | INTEL(R) XEON(R) PLATINUM 8573C | `sha256:9e5fddac948e3f8d7b9b2b18d1f393412e8edcad0514c289ae7030fb7c262690` | +| 2 | INTEL(R) XEON(R) PLATINUM 8573C | `sha256:f6e54e4ad2fa2271d8785f931fbdc8ddbdd77c535d1de4db9ccb5dfa559236be` | +| 3 | INTEL(R) XEON(R) PLATINUM 8573C | `sha256:6bf176439724415e8487be66c030d7dce23a24ea642338319c0c49001eae03d7` | + +The scalar Actions reports retain every original, block order, cache counters, SQL and history SELECT counts/elapsed, task/timer delay distributions, source hashes, cold test-worker startup, CPU/RSS/cgroup peak and exit evidence. No database/history archive is published. diff --git a/tests/Performance/results/issue-748-20261020/GROWTH-1000-PILOT.md b/tests/Performance/results/issue-748-20261020/GROWTH-1000-PILOT.md new file mode 100644 index 000000000..b2cdb123c --- /dev/null +++ b/tests/Performance/results/issue-748-20261020/GROWTH-1000-PILOT.md @@ -0,0 +1,32 @@ +# 1,000-event embedded pilot, October 20, 2026 + +[Run 39452413954](https://github.com/durable-workflow/workflow/actions/runs/39452413954) completes both fresh originals under their unchanged bounds. Source `ae29003f8308869c3aca84dd0c0ce61b3e0eb31e`, tree `14dffaf0485a0795c85dc78297682825ed8762b8`, one worker/run, one CPU / 1 GiB aggregate memory / no swap. Each mode has its own original 3,600-second run/execution/observation bound, 30-second startup and 15-second drain. The older one-hour failed original remains failed; these are independent observations. + +This is one alternating-mode pair, not the required three repetitions or qualification of later claim/observer changes. Intel Xeon Platinum 8573C; built image `sha256:7e9f2b8d2fdc2ad22edc86c6ccf284ca8feb70d57fe1436871a4cb815206c3e7`; PHP 8.5.6, Laravel 13.38.0, SQLite 3.51.2, input lock SHA-256 `0e41dd1ce1043d7ab1493a9916f219c571a9b931cc4f448d0b73da2b535eef8d`. Both use the same source, image, lock, queue, real one-second timers and observers. Exact loaded-source hashes are checked for controller/workers. + +Both originals pass 1,000 contiguous events, 199 typed activity effects, 799 completed first-attempt tasks, 200 durable timers, preserved admission prefix/original deadlines, correct recorded clocks/results, zero repair counts/failed jobs, empty transport and successful worker drain. Neither container was OOM-killed or restarted. + +| Measurement | Replay | Retained | +|---|---:|---:| +| Workload wall seconds | 2319.014 | 1391.140 | +| Queue-worker CPU seconds | 2097.221 | 1170.469 | +| Cgroup workload CPU seconds | 2104.815 | 1174.869 | +| Whole-container peak MiB | 198.61 | 198.67 | +| Worker peak RSS MiB | 126.66 | 127.06 | +| Authored entries | 400 | 39 | +| Authored round visits | 40198 | 4819 | +| History SELECTs | 4393 | 5192 | +| All SQL statements | 266452 | 266781 | + +Retained execution records 361 hits / 39 misses, 31 age evictions and seven age publication discards. The nonrenewing 30-second age, four-entry bound and 128 MiB worker-RSS threshold are unchanged. Workers end with zero retained entries. There is no affinity wait or lease extension. + +Within this single pair, worker CPU is 44.19% lower and workload wall is 40.01% lower. History SELECTs rise from 4,393 to 5,192. Worker CPU includes history/model loading, replay/forward execution, history/projection, dispatch, transaction, activity and observer work; it does not isolate replay CPU. This pair supports further qualification, not a production speedup/capacity claim or a pooled estimate. + +| Delay diagnostic | Replay | Retained | +|---|---:|---:| +| Task-claim delay p50 ms | 699.035 | 714.283 | +| Task-claim delay p95 ms | 1744.408 | 1749.552 | +| Timer lateness p50 ms | 1432.854 | 1460.503 | +| Timer lateness p95 ms | 2276.317 | 2150.371 | + +Task delay has 799 observations per original; timer lateness has 200. They are separate elapsed-time distributions, not CPU or a pure transport-overhead estimate. Cold test-worker startup, SQL elapsed, source/image/lock and exit evidence remain in the scalar Actions logs. No database/history archive is published. diff --git a/tests/Performance/results/issue-748-20261020/GROWTH-1000.md b/tests/Performance/results/issue-748-20261020/GROWTH-1000.md new file mode 100644 index 000000000..8788ad1e1 --- /dev/null +++ b/tests/Performance/results/issue-748-20261020/GROWTH-1000.md @@ -0,0 +1,69 @@ +# 1,000-event original studies, October 20, 2026 + +These observations preserve every failed original and qualify only their stated source. They do not establish release or customer capacity. Each mode has a fresh one-CPU / 1 GiB / no-swap container, real database queues and one-second durable timers, with its original 3,600-second execution/run/observation bound, 30-second startup and 15-second drain. No failed original was resumed, retried or rebudgeted. PHP 8.5.6, Laravel 13.38.0, SQLite 3.51.2; input lock SHA-256 `0e41dd1ce1043d7ab1493a9916f219c571a9b931cc4f448d0b73da2b535eef8d`. + +## Single worker, single run + +[Run 39462444846](https://github.com/durable-workflow/workflow/actions/runs/39462444846), source `e3f148bc307534b52162c1e5e9d2abe6a06fdef2`, passes all six original cohorts. Every cohort verifies 1,000 contiguous events, 199 typed activity effects, 799 first-attempt tasks, 200 durable timers, identical recorded clocks/results and admission prefix, original deadlines, empty transport and graceful drain. There are zero task repairs, failed jobs, unfinished runs, OOM kills or restarts. This source predates the configured-model and broker-acknowledgement corrections. + +Wall/CPU are seconds; peak is whole-container MiB. Worker CPU includes history/projection, hydration, dispatch, activities and observer work. It is not isolated replay or completed-prefix CPU. Alternating order reduces ordering bias but does not remove same-host drift. + +| Repetition | Replay wall / CPU | Retained wall / CPU | Peak replay / retained MiB | Retained hits / misses | Retained cold entries | +|---|---:|---:|---:|---:|---:| +| 1 | 1648.952 / 1363.637 | 956.231 / 659.998 | 198.87 / 200.20 | 371 / 29 | 29 | +| 2 | 2653.723 / 2472.823 | 1569.279 / 1367.728 | 198.86 / 198.85 | 357 / 43 | 43 | +| 3 | 2792.010 / 2612.896 | 1647.070 / 1454.366 | 193.50 / 194.18 | 356 / 44 | 44 | + +Replay enters authored PHP 400 times / 40,198 round visits per cohort. Retained execution enters 29/43/44 times / 3,530/5,397/5,485 visits. History SELECTs increase from 4,393 to 5,192 per run; retention does not remove authoritative history/projection work. Every retained cohort validates 200,399 committed history events. Nonrenewing age, four-entry limit and worker RSS threshold are unchanged, with zero entries after drain. Worker peak RSS spans 125.60–128.52 MiB. These process-wide CPU differences support continued bounded retention qualification; they are not a production speedup estimate. + +| Repetition | CPU model | Built image | +|---|---|---| +| 1 | AMD EPYC 9V45 96-Core Processor | `sha256:2948f3b5c1f3fb955a1ac157545565cdce7f2faaad969903e91cc35022441659` | +| 2 | AMD EPYC 7763 64-Core Processor | `sha256:2572fc49363185de885d2cab02c321923073b690bc60ce3f7dc70fdd2c9e1438` | +| 3 | AMD EPYC 7763 64-Core Processor | `sha256:36e01cb9d47d712e950e8d45499c4f50efc02a92dac1bd555393bffe53d5c94b` | + +## Four workers, single run + +[Run 39463828599](https://github.com/durable-workflow/workflow/actions/runs/39463828599), source `9ee8d64115f62e8a6ea0ba1fa9b94021bcc4f89d`, fails all six original cohorts. Each has its sole workflow unfinished. Three containers report OOM; the remaining three report ordinary Laravel worker memory-limit exits (12). The per-worker Laravel allocation threshold is 256 MiB, not an RSS hard limit. All six have failed broker jobs, with available exception samples showing SQLite `database is locked` in projection writes. No task has a repair-count increment. Root cause and applicability to released cold execution are unproven; this is not evidence of customer impact or a retained-frame-specific defect. + +| Repetition / mode | Elapsed seconds | Events / effects | Completed / total tasks | Failed jobs | Container OOM | +|---|---:|---:|---:|---:|---| +| 1 / replay | 1278.776 | 729 / 146 | 581 / 582 | 40 | yes | +| 1 / retained | 1258.415 | 760 / 152 | 606 / 607 | 41 | yes | +| 2 / replay | 1346.031 | 832 / 166 | 664 / 665 | 38 | no; worker memory exit | +| 2 / retained | 1213.359 | 856 / 171 | 683 / 684 | 32 | no; worker memory exit | +| 3 / replay | 1387.909 | 830 / 166 | 662 / 663 | 37 | yes | +| 3 / retained | 1258.486 | 840 / 168 | 670 / 671 | 37 | no; worker memory exit | + +Failures occurred before the original observation deadline. Each container exits 1 with zero restarts. The failure path retained counts, bounded worker-log tails and OOM flags, but did not publish final worker CPU/RSS JSON. Those values are unavailable; no successful-subset speedup or full semantic pass is inferred. A subsequent diagnostic-only change publishes any already available final worker reports and current process exit states before cleanup; it cannot recover or replace these observations. + +## Eight-run admission pilots + +Both pilots use source `9ee8d64115f62e8a6ea0ba1fa9b94021bcc4f89d`, the same original limits and repetition 1. [One worker / eight runs](https://github.com/durable-workflow/workflow/actions/runs/39463831372) and [four workers / eight runs](https://github.com/durable-workflow/workflow/actions/runs/39463834650) each fail both independent modes at the original observation deadline. All eight workflows remain unfinished in every cohort; no complete semantic or drain audit follows. + +| Workers / mode | Elapsed seconds | Events / effects | Completed / total tasks | Tasks with positive repair count | Failed jobs | +|---|---:|---:|---:|---:|---:| +| 1 / replay | 3599.893 | 5123 / 1022 | 4085 / 4093 | 0 | 0 | +| 1 / retained | 3599.851 | 5151 / 1027 | 4108 / 4116 | 0 | 0 | +| 4 / replay | 3599.773 | 3767 / 751 | 3000 / 3008 | 7 | 57 | +| 4 / retained | 3599.816 | 3822 / 762 | 3044 / 3052 | 11 | 62 | + +All four containers exit 1 without OOM or restart. The four-worker samples include SQLite lock exceptions; retained execution also samples `MaxAttemptsExceededException`. The positive repair counts are not classified as healthy pre-claim redispatch or lease recovery without corresponding evidence. Final worker CPU/RSS reports were not published by this older failure path and remain unavailable. Partial progress is not throughput acceptance or evidence of equivalent completed results. + +The one-worker host is AMD EPYC 9V45, built image `sha256:d36b47b1bf5f13ffd5dd3a0fbd5703db9d5b81303d5a7641ec7dc9a6e71e1a8f`. The four-worker host is AMD EPYC 7763, image `sha256:fa52f1c48e248c04c0ff6630a153cdc9ac394c07f236f1622ff650f4aac941f0`. + +The predeclared admission rule requires pilot review before the remaining large concurrent repetitions. These failed pilots do not justify admitting those repetitions. They remain unrun; this report does not claim a completed 72-original grid. The completed three-pair smaller and single-run comparisons support an opt-in, workload-specific feature with explicit capacity limits. They do not establish a safe concurrent SQLite envelope, and retention is not a remedy for the failed capacity cells. + +## CPU interpretation and product decision + +The successful single-worker study separates actual Fiber execution from history/projection and repeated durable dispatch. Exclusive CPU seconds (nested buckets excluded) are: + +| Repetition | History/projection replay / retained | Durable dispatch replay / retained | Step validation replay / retained | Authored PHP replay / retained | +|---|---:|---:|---:|---:| +| 1 | 603.216 / 546.736 | 694.478 / 71.487 | 25.759 / 2.581 | 3.005 / 0.182 | +| 2 | 1103.905 / 1090.913 | 1263.542 / 203.501 | 47.015 / 7.469 | 3.789 / 0.527 | +| 3 | 1153.902 / 1155.735 | 1344.703 / 218.270 | 50.882 / 8.121 | 5.252 / 0.723 | + +Repeated durable dispatch and validation dominate the observed saving, rather than authored PHP itself. Dispatch/validation calls decrease from 80,200 to 7,419 / 11,133 / 11,307; history/projection role calls remain 2,396 in each mode. These phases include forward execution, and the process-wide observer adds work. They do not isolate completed-prefix CPU or establish a general production correction factor. The [same-host observer/drift study](EXECUTOR-CPU.md) preserves that uncertainty. + +The selected product remains bounded, explicitly enabled continuation for eligible sequential workflows. History/projection stays authoritative; unsupported features and missing or invalid local frames use ordinary replay. Operators must qualify their own history size, database and concurrency, retain normal worker supervision, and account for both additional history reads and cache misses. Earlier failed one-hour and zero-repair audit originals remain failed. Later source qualification is separately attributed. No full history/database archive is exported. diff --git a/tests/Performance/results/issue-748-20261020/GROWTH.md b/tests/Performance/results/issue-748-20261020/GROWTH.md new file mode 100644 index 000000000..c23c0e8c9 --- /dev/null +++ b/tests/Performance/results/issue-748-20261020/GROWTH.md @@ -0,0 +1,58 @@ +# Current-main small growth comparison + +This source study covers twelve independent originals: 10/100 events, one persistent worker, one concurrent run, replay/retained modes and three repetitions. It does not qualify larger histories, concurrency, final retained-execution source or published artifacts. Earlier unsuccessful originals remain unchanged. + +## Source and conditions + +- Source: `0648f9f6a0f5f3ccc06beb89c77959e67ce8f548`; runtime equals `178ad639` and incorporates released 2.7.3 main. +- Input: [committed Composer lock](https://github.com/durable-workflow/workflow/blob/0648f9f6a0f5f3ccc06beb89c77959e67ce8f548/tests/Performance/continuation/COMPOSER-INPUT.lock), SHA-256 `0e41dd1ce1043d7ab1493a9916f219c571a9b931cc4f448d0b73da2b535eef8d`. +- PHP 8.5.6, Laravel 13.38.0, SQLite 3.51.2 and Avro 1.12.2. The image uses the committed Dockerfile and immutable Composer base, with actual pcntl support. +- Each pair shares its runner, image, source and dependencies. Each mode uses a fresh container/database with one CPU, 1 GiB aggregate memory, no swap and no network. Hosted CPU/kernel models vary across pairs. +- Full optional observers remain on. Each original retains its 3,600-second execution/run/observation budgets, 30-second worker startup bound and 15-second transport/worker drain bound. +- Three alternating replay/retained pairs per size. Actual database queues, typed activity effects and one-second timers run without mocks or virtual time. + +[10-event Actions evidence](https://github.com/durable-workflow/workflow/actions/runs/39449709274) and [100-event Actions evidence](https://github.com/durable-workflow/workflow/actions/runs/39450106760) contain every original report and teardown result. All twelve pass the exact history, result, clock, original deadline, task/effect, source/lock, queue-empty and exit audits. Total: 660 events, 120 unique activity effects, 516 completed tasks and 132 timers, with zero repairs, failed jobs, errors or unfinished runs. Every container exits zero with no OOM or restart. + +## Per-original results + +CPU and wall values are seconds. Peak MiB is the complete fresh-container cgroup peak, including startup/audit. Worker CPU includes replay, projection, history/model loading, dispatch and activity/timer execution. It is not isolated replay CPU. Worker cold-start CPU is measured separately and is not included in queue CPU. + +| Events | Rep | Mode | Worker CPU | Queue wall | Aggregate peak MiB | Authored entries | Hits / misses | History SELECTs | +| ---: | ---: | --- | ---: | ---: | ---: | ---: | --- | ---: | +| 10 | 1 | replay | 0.658683 | 6.787323 | 132.84 | 4 | disabled | 37 | +| 10 | 1 | retained | 0.629675 | 6.737483 | 133.20 | 1 | 3 / 1 | 44 | +| 10 | 2 | replay | 0.383904 | 6.470602 | 133.21 | 4 | disabled | 37 | +| 10 | 2 | retained | 0.384447 | 6.521443 | 133.11 | 1 | 3 / 1 | 44 | +| 10 | 3 | replay | 0.597610 | 6.732288 | 133.20 | 4 | disabled | 37 | +| 10 | 3 | retained | 0.618205 | 6.734843 | 133.27 | 1 | 3 / 1 | 44 | +| 100 | 1 | replay | 14.251240 | 56.073207 | 145.82 | 40 | disabled | 433 | +| 100 | 1 | retained | 13.207043 | 54.963437 | 145.54 | 2 | 38 / 2 | 512 | +| 100 | 2 | replay | 13.113439 | 63.871097 | 145.93 | 40 | disabled | 433 | +| 100 | 2 | retained | 11.819803 | 62.782629 | 145.76 | 2 | 38 / 2 | 512 | +| 100 | 3 | replay | 16.346517 | 55.949345 | 150.43 | 40 | disabled | 433 | +| 100 | 3 | retained | 15.297247 | 55.901072 | 150.57 | 2 | 38 / 2 | 512 | + +At ten events, paired CPU changes vary in sign. At 100 events, retained worker CPU is 7.33%, 9.86% and 6.42% lower within the three respective host pairs. These descriptive differences do not establish production savings or an isolated replay saving. No median is pooled across different host models. + +Each 100-event retained original avoids 38 completed-prefix re-entries but makes 512 history SELECTs versus 433 for replay. Each has one ordinary nonrenewing age eviction or publication discard. Neither database-read reduction nor removal of history/projection cost is established. SQL elapsed time, all-query counts, seven/79 task-claim delay observations and two/20 timer-lateness observations per respective size remain separate scalar distributions in the Actions reports. They are not database CPU or pure queue overhead. + +## Runner/image identities + +The image was built once per pair from the same pinned source and base; image IDs differ between hosted builds. Within each pair both modes use the same exact image. + +| Events | Rep | Reported CPU model | Image ID | +| ---: | ---: | --- | --- | +| 10 | 1 | AMD EPYC 7763 64-Core Processor | `sha256:22e1f2f563b1b72a8502caf060e8cdec427714bd9ca1bbbfce7f0ff85df1c0a9` | +| 10 | 2 | AMD EPYC 9V45 96-Core Processor | `sha256:3a28b3eab439bd26c34e7ccbaeda8917fef3fb01fbc2d64baae6ab12ee8019ea` | +| 10 | 3 | AMD EPYC 7763 64-Core Processor | `sha256:3d4c4b6921d36595b0d1cbe454c1834816dded5597518379d2b22432646ffa24` | +| 100 | 1 | INTEL(R) XEON(R) PLATINUM 8573C | `sha256:eb8f939f1c0438ee26fcc094ab39fe8d9b699b3f2fa408559ef08d242eb729b8` | +| 100 | 2 | AMD EPYC 9V74 80-Core Processor | `sha256:83a3a3f3da15c25ef029991f78f7e9f65512fcf3e183b545470bbb04321b63ac` | +| 100 | 3 | AMD EPYC 7763 64-Core Processor | `sha256:129dbc4a68136565281b50bc136ff36a724ca1016e97601ba4ca50a4eafe098c` | + +Cold test-worker startup CPU ranges from 0.123019 to 0.219365 seconds. It includes PHPUnit/Testbench startup, source checks and opening SQLite, rather than a stock Artisan-only startup. Hosted kernels are Linux 7.0.0-1017-azure except 100-event repetition three, which uses 6.17.0-1022-azure. + +## Next acceptance + +The small-cell cost supports a bounded next 1,000-event pilot. It does not authorize stable retention. The later database-session identity correction and remaining lifetime controls require their own exact-source qualification. The final tracing integration, replay-phase attribution, concurrent/multiworker hit/miss/pressure cells, complete regression/conformance, published package and affected consumers remain required. No old whole-tree pass or these source measurements transfer to a later candidate. + +Only readable measurements and reproducible source/lock remain after audit. Actions cleanup removes disposable installations; containers/databases are task-owned and removed. No database/history archive is retained. diff --git a/tests/Support/ContinuationGrowthEnvironment.php b/tests/Support/ContinuationGrowthEnvironment.php new file mode 100644 index 000000000..85d6fb0ac --- /dev/null +++ b/tests/Support/ContinuationGrowthEnvironment.php @@ -0,0 +1,99 @@ +set('database.default', 'growth'); + $app['config']->set('database.connections.growth', [ + 'driver' => 'sqlite', + 'database' => $directory . '/database.sqlite', + 'prefix' => '', + 'foreign_key_constraints' => true, + 'busy_timeout' => 5000, + ]); + $app['config']->set('workflows.storage.connection', 'growth'); + $app['config']->set('workflows.v1.enabled', false); + $app['config']->set('workflows.v2.task_dispatch_mode', 'queue'); + $app['config']->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [TestContinuationGrowthWorkflow::class] : [], + 'build' => 'continuation-growth-v1', + ]); + $app['config']->set('queue.default', 'database'); + $app['config']->set('queue.connections.database', [ + 'driver' => 'database', + 'connection' => 'growth', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 360, + ]); + $app['config']->set('queue.failed', [ + 'driver' => 'database-uuids', + 'database' => 'growth', + 'table' => 'failed_jobs', + ]); + $app['config']->set('cache.default', 'array'); + $app['config']->set('logging.default', 'null'); + } + + public static function cpuMicroseconds(): int + { + $usage = getrusage(); + + return ((int) $usage['ru_utime.tv_sec'] + (int) $usage['ru_stime.tv_sec']) * 1_000_000 + + (int) $usage['ru_utime.tv_usec'] + (int) $usage['ru_stime.tv_usec']; + } + + public static function loadedSources(): array + { + $loaded = []; + foreach ([WorkflowExecutor::class, RunWorkflowTask::class, WorkflowContinuationCache::class, + WorkflowContinuationSession::class, WorkflowExecution::class, + \Workflow\V2\Support\WorkflowExecutionProfile::class, ContinuationHistoryProfile::class, + ContinuationTaskMaintenance::class, + TestContinuationGrowthWorkflow::class, TestContinuationGrowthActivity::class] as $class) { + $loaded[$class] = hash_file('sha256', (new ReflectionClass($class))->getFileName()); + } + + return $loaded; + } + + public static function distribution(array $values): array + { + sort($values); + $count = count($values); + + return [ + 'count' => $count, + 'min_us' => $values[0] ?? null, + 'p50_us' => $count > 0 ? $values[(int) floor(($count - 1) * 0.5)] : null, + 'p95_us' => $count > 0 ? $values[(int) floor(($count - 1) * 0.95)] : null, + 'max_us' => $values[$count - 1] ?? null, + ]; + } + + public static function writeReport(string $path, array $report): void + { + file_put_contents($path . '.pending', json_encode($report, JSON_THROW_ON_ERROR)); + rename($path . '.pending', $path); + } +} diff --git a/tests/Support/ContinuationGrowthWorkerTest.php b/tests/Support/ContinuationGrowthWorkerTest.php new file mode 100644 index 000000000..2cdfd0eea --- /dev/null +++ b/tests/Support/ContinuationGrowthWorkerTest.php @@ -0,0 +1,172 @@ +markTestSkipped('Invoked by EmbeddedContinuationGrowthProfileTest.'); + } + $reportPath = $directory . '/worker-' . (int) getenv('DWF_GROWTH_WORKER'); + $loaded = ContinuationGrowthEnvironment::loadedSources(); + $sqlite = DB::selectOne('SELECT sqlite_version() AS version')->version; + $async = function_exists('pcntl_async_signals') && function_exists('pcntl_signal'); + $observers = getenv('DWF_GROWTH_OBSERVERS') !== 'off'; + if (getenv('PROFILE_EMBEDDED_CONTINUATION') === 'measure') { + $this->assertTrue($async, 'Qualified measurements require the actual worker timeout/signal path.'); + } + $startupCpu = ContinuationGrowthEnvironment::cpuMicroseconds(); + $maintenance = new ContinuationTaskMaintenance(); + WorkflowTask::updated($maintenance->observe(...)); + $profile = $observers ? new WorkflowExecutionProfile() : null; + if ($profile !== null) { + $delegate = app(HistoryProjectionRole::class); + $this->assertInstanceOf(DefaultHistoryProjectionRole::class, $delegate); + $this->app->instance(HistoryProjectionRole::class, new ContinuationHistoryProfile($delegate)); + } + ContinuationGrowthEnvironment::writeReport($reportPath . '.ready', [ + 'pid' => getmypid(), + 'startup_cpu_us' => $startupCpu, + 'loaded_sources' => $loaded, + 'async_signals' => $async, + 'observers_enabled' => $observers, + 'php' => PHP_VERSION, + 'laravel' => $this->app->version(), + 'sqlite' => $sqlite, + 'lock_sha256' => hash_file('sha256', dirname(__DIR__, 2) . '/composer.lock'), + ]); + while (! is_file($directory . '/start')) { + usleep(10_000); + } + $peakEntries = $observers ? 0 : null; + $this->app['events']->listen(Looping::class, function () use ($directory, $observers, &$peakEntries): void { + if ($observers) { + $peakEntries = max($peakEntries, app(WorkflowContinuationCache::class)->snapshot()['entries']); + } + if (is_file($directory . '/stop')) { + $worker = $this->app->make('queue.worker'); + $worker->shouldQuit = true; + } + }); + WorkflowRun::updated(static function (WorkflowRun $run) use ($directory): void { + if ($run->status === RunStatus::Completed && $run->wasChanged('status')) { + $run->getConnection() + ->afterCommit(static fn () => touch($directory . '/completed-' . $run->id)); + } + }); + $claims = []; + $timers = []; + $queries = 0; + $queryMilliseconds = 0.0; + $historySelects = 0; + $historyMilliseconds = 0.0; + if ($observers) { + $historyTable = (new WorkflowHistoryEvent())->getTable(); + WorkflowTask::updated(static function (WorkflowTask $task) use (&$claims): void { + if ($task->status === TaskStatus::Leased && $task->wasChanged('status') + && $task->available_at !== null && $task->leased_at !== null) { + $claims[$task->id . ':' . $task->attempt_count] = $task->available_at->diffInMicroseconds( + $task->leased_at, + false + ); + } + }); + WorkflowTimer::updated(static function (WorkflowTimer $timer) use (&$timers): void { + if ($timer->status === TimerStatus::Fired && $timer->wasChanged('status')) { + $timers[$timer->id] = $timer->fire_at->diffInMicroseconds($timer->fired_at, false); + } + }); + DB::listen(static function (QueryExecuted $query) use ( + &$queries, + &$queryMilliseconds, + &$historySelects, + &$historyMilliseconds, + $historyTable + ): void { + ++$queries; + $queryMilliseconds += $query->time; + if (strncasecmp(ltrim($query->sql), 'select', 6) === 0 + && str_contains($query->sql, $historyTable)) { + ++$historySelects; + $historyMilliseconds += $query->time; + } + }); + } + TestContinuationGrowthWorkflow::$entries = 0; + TestContinuationGrowthWorkflow::$roundVisits = 0; + $started = hrtime(true); + $cpu = ContinuationGrowthEnvironment::cpuMicroseconds(); + $work = static fn (): int => Artisan::call('queue:work', [ + 'connection' => 'database', + '--queue' => 'default', + '--sleep' => 1, + '--max-time' => (int) getenv('DWF_GROWTH_BUDGET'), + '--timeout' => 300, + '--tries' => 1, + '--memory' => (int) getenv('DWF_GROWTH_MEMORY'), + ]); + $exit = $profile === null ? $work() : $profile->collect($work); + $elapsed = hrtime(true) - $started; + $cpu = ContinuationGrowthEnvironment::cpuMicroseconds() - $cpu; + ContinuationGrowthEnvironment::writeReport($reportPath . '.json', [ + 'pid' => getmypid(), + 'exit' => $exit, + 'observers_enabled' => $observers, + 'queue_cpu_us' => $cpu, + 'queue_wall_ns' => $elapsed, + 'startup_cpu_us' => $startupCpu, + 'peak_rss_kib' => getrusage()['ru_maxrss'], + 'peak_allocated_bytes' => memory_get_peak_usage(true), + 'queries' => $observers ? $queries : null, + 'query_elapsed_ms' => $observers ? $queryMilliseconds : null, + 'history_selects' => $observers ? $historySelects : null, + 'history_select_elapsed_ms' => $observers ? $historyMilliseconds : null, + 'entries' => TestContinuationGrowthWorkflow::$entries, + 'round_visits' => TestContinuationGrowthWorkflow::$roundVisits, + 'cache' => app(WorkflowContinuationCache::class)->snapshot(), + 'task_maintenance' => $maintenance->snapshot(), + 'execution_profile' => $profile?->snapshot(), + 'observed_peak_entries' => $peakEntries, + 'task_queue_delay' => $observers ? ContinuationGrowthEnvironment::distribution($claims) : null, + 'timer_lateness' => $observers ? ContinuationGrowthEnvironment::distribution($timers) : null, + ]); + $this->assertSame(0, $exit, Artisan::output()); + } + + protected function getPackageProviders($app): array + { + return ContinuationGrowthEnvironment::providers(); + } + + protected function defineEnvironment($app): void + { + $directory = getenv('DWF_GROWTH_DIRECTORY'); + if (is_string($directory) && $directory !== '') { + ContinuationGrowthEnvironment::configure($app, $directory, getenv('DWF_GROWTH_RETAINED') === '1'); + } + } +} diff --git a/tests/Support/ContinuationHistoryProfile.php b/tests/Support/ContinuationHistoryProfile.php new file mode 100644 index 000000000..b8b20eb6e --- /dev/null +++ b/tests/Support/ContinuationHistoryProfile.php @@ -0,0 +1,98 @@ +measure(fn () => $this->delegate->projectRun($run)); + } + + public function recordActivityStarted( + WorkflowRun $run, + ActivityExecution $execution, + ActivityAttempt $attempt, + WorkflowTask $task + ): WorkflowRunSummary { + return $this->measure(fn () => $this->delegate->recordActivityStarted($run, $execution, $attempt, $task)); + } + + public function projectRepairedWorkflowTask(WorkflowRun $run, WorkflowTask $task): WorkflowRunSummary + { + return $this->measure(fn () => $this->delegate->projectRepairedWorkflowTask($run, $task)); + } + + public function bufferedSignalSummary(WorkflowRun $run): ?WorkflowRunSummary + { + return $this->delegate->bufferedSignalSummary($run); + } + + public function projectBufferedSignal( + WorkflowRun $run, + WorkflowHistoryEvent $event, + WorkflowRunSummary $summary + ): WorkflowRunSummary { + return $this->delegate->projectBufferedSignal($run, $event, $summary); + } + + /** + * @param class-string<\Illuminate\Database\Eloquent\Model> $projectionModel + * @param list $seenProjectionIds + */ + public function pruneStaleProjectionRowsForRun( + string $projectionModel, + string $runId, + array $seenProjectionIds + ): void { + $this->delegate->pruneStaleProjectionRowsForRun($projectionModel, $runId, $seenProjectionIds); + } + + /** + * @param list $runIds + * @return array{ + * run_summaries: int, + * run_waits: int, + * run_timeline_entries: int, + * run_timer_entries: int, + * run_lineage_entries: int + * } + */ + public function pruneStaleProjections(array $runIds = [], ?string $instanceId = null, bool $dryRun = false): array + { + return $this->delegate->pruneStaleProjections($runIds, $instanceId, $dryRun); + } + + /** + * @param callable(): WorkflowRunSummary $operation + */ + private function measure(callable $operation): WorkflowRunSummary + { + $sample = WorkflowExecutionProfile::begin('history_projection'); + try { + return $operation(); + } finally { + WorkflowExecutionProfile::finish($sample); + } + } +} diff --git a/tests/Support/ContinuationProcessEnvironment.php b/tests/Support/ContinuationProcessEnvironment.php new file mode 100644 index 000000000..757a259ca --- /dev/null +++ b/tests/Support/ContinuationProcessEnvironment.php @@ -0,0 +1,52 @@ +set('database.default', 'process'); + $app['config']->set('database.connections.process', [ + 'driver' => 'sqlite', + 'database' => $database, + 'prefix' => '', + 'foreign_key_constraints' => true, + 'busy_timeout' => 1000, + ]); + $app['config']->set('workflows.storage.connection', 'process'); + $app['config']->set('workflows.v1.enabled', false); + $app['config']->set('workflows.v2.task_dispatch_mode', 'queue'); + $app['config']->set(WorkflowTaskLease::CONFIG_KEY, 3); + $app['config']->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [TestContinuationProcessWorkflow::class] : [], + 'build' => 'process-loss-test', + ]); + $app['config']->set('queue.default', 'database'); + $app['config']->set('queue.connections.database', [ + 'driver' => 'database', + 'connection' => 'process', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 4, + ]); + $app['config']->set('queue.failed', [ + 'driver' => 'database-uuids', + 'database' => 'process', + 'table' => 'failed_jobs', + ]); + $app['config']->set('cache.default', 'array'); + $app['config']->set('logging.default', 'null'); + } +} diff --git a/tests/Support/ContinuationProcessWorkerTest.php b/tests/Support/ContinuationProcessWorkerTest.php new file mode 100644 index 000000000..81a19dc7d --- /dev/null +++ b/tests/Support/ContinuationProcessWorkerTest.php @@ -0,0 +1,93 @@ +markTestSkipped('Invoked by EmbeddedContinuationProcessTest with its existing database.'); + } + $checkpoint = getenv('DWF_PROCESS_CHECKPOINT'); + if (is_string($checkpoint) && $checkpoint !== '') { + WorkflowTask::updated(static function (WorkflowTask $task) use ($checkpoint): void { + if ($task->task_type !== TaskType::Workflow || $task->status !== TaskStatus::Leased + || ! WorkflowHistoryEvent::query()->where('workflow_run_id', $task->workflow_run_id) + ->where('event_type', HistoryEventType::TimerFired->value)->exists()) { + return; + } + $database = $task->getConnection(); + $database->afterCommit(static function () use ($task, $database, $checkpoint): void { + $snapshot = [ + 'pid' => getmypid(), + 'task_id' => $task->id, + 'run_id' => $task->workflow_run_id, + 'lease' => $task->getRawOriginal('lease_expires_at'), + 'transaction_level' => $database->transactionLevel(), + 'pdo_transaction' => $database->getPdo() + ->inTransaction(), + 'cache' => app(WorkflowContinuationCache::class)->snapshot(), + ]; + file_put_contents($checkpoint . '.pending', json_encode($snapshot, JSON_THROW_ON_ERROR)); + rename($checkpoint . '.pending', $checkpoint); + // Pause after a committed claim, before that task can consume the cached frame. + while (true) { + usleep(100_000); + } + }); + }); + } + $peakEntries = 0; + $this->app['events']->listen(JobProcessed::class, static function () use (&$peakEntries): void { + $peakEntries = max($peakEntries, app(WorkflowContinuationCache::class)->snapshot()['entries']); + }); + $memory = (int) (getenv('DWF_PROCESS_MEMORY') ?: 256); + $exit = Artisan::call('queue:work', [ + 'connection' => 'database', + '--queue' => 'default', + '--sleep' => 1, + '--max-jobs' => (int) getenv('DWF_PROCESS_MAX_JOBS'), + '--timeout' => 15, + '--memory' => $memory, + ]); + $report = getenv('DWF_PROCESS_REPORT'); + if (is_string($report) && $report !== '') { + file_put_contents($report, json_encode([ + 'command_exit' => $exit, + 'peak_entries' => $peakEntries, + 'cache' => app(WorkflowContinuationCache::class)->snapshot(), + 'transaction_level' => DB::connection()->transactionLevel(), + 'pdo_transaction' => DB::connection()->getPdo()->inTransaction(), + ], JSON_THROW_ON_ERROR)); + } + $this->assertSame($memory === 1 ? 12 : 0, $exit, Artisan::output()); + } + + protected function getPackageProviders($app): array + { + return ContinuationProcessEnvironment::providers(); + } + + protected function defineEnvironment($app): void + { + $database = getenv('DWF_PROCESS_DATABASE'); + if (is_string($database) && $database !== '') { + ContinuationProcessEnvironment::configure($app, $database, getenv('DWF_PROCESS_ENABLED') === '1'); + } + } +} diff --git a/tests/Support/ContinuationTaskMaintenance.php b/tests/Support/ContinuationTaskMaintenance.php new file mode 100644 index 000000000..fde40d662 --- /dev/null +++ b/tests/Support/ContinuationTaskMaintenance.php @@ -0,0 +1,48 @@ + 0, + 'lease_recoveries' => 0, + 'other_repairs' => 0, + ]; + + public function observe(WorkflowTask $task): void + { + if (! $task->wasChanged('repair_count')) { + return; + } + $change = (int) $task->repair_count - (int) $task->getRawOriginal('repair_count'); + $before = $task->getRawOriginal('status'); + $ready = $task->status === TaskStatus::Ready; + $healthy = in_array($task->getRawOriginal('last_dispatch_error'), [null, ''], true) + && in_array($task->getRawOriginal('last_claim_error'), [null, ''], true); + $kind = match (true) { + $change > 0 && $before === TaskStatus::Ready->value && $ready && $healthy + && (int) $task->getRawOriginal('attempt_count') === 0 => 'ready_redispatches', + $change > 0 && $before === TaskStatus::Leased->value && $ready => 'lease_recoveries', + default => 'other_repairs', + }; + $this->counts[$kind] += abs($change); + } + + /** + * @return array{ready_redispatches: int, lease_recoveries: int, other_repairs: int} + */ + public function snapshot(): array + { + return $this->counts; + } +} diff --git a/tests/Unit/V2/EmbeddedContinuationProcessTest.php b/tests/Unit/V2/EmbeddedContinuationProcessTest.php new file mode 100644 index 000000000..b8ab5bef2 --- /dev/null +++ b/tests/Unit/V2/EmbeddedContinuationProcessTest.php @@ -0,0 +1,302 @@ + + */ + private array $workers = []; + + protected function setUp(): void + { + $this->directory = sys_get_temp_dir() . '/dw-process-loss-' . bin2hex(random_bytes(8)); + mkdir($this->directory, 0700); + touch($this->directory . '/database.sqlite'); + parent::setUp(); + $this->artisan('migrate', [ + '--force' => true, + ])->run(); + DB::statement('CREATE TABLE jobs (id INTEGER PRIMARY KEY AUTOINCREMENT, + queue TEXT NOT NULL, payload TEXT NOT NULL, attempts INTEGER NOT NULL, + reserved_at INTEGER, available_at INTEGER NOT NULL, created_at INTEGER NOT NULL)'); + DB::statement('CREATE TABLE failed_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL UNIQUE, connection TEXT NOT NULL, queue TEXT NOT NULL, + payload TEXT NOT NULL, exception TEXT NOT NULL, failed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)'); + } + + protected function tearDown(): void + { + foreach ($this->workers as $worker) { + if ($worker->isRunning()) { + $worker->stop(0, 9); + } + } + parent::tearDown(); + (new Filesystem())->deleteDirectory($this->directory); + } + + public static function profiles(): array + { + return [ + 'ordinary cold worker' => [false, 2, 3], + 'retained worker' => [true, 1, 2], + ]; + } + + public static function memoryStopProfiles(): array + { + return [ + 'ordinary cold worker' => [false], + 'retained worker' => [true], + ]; + } + + #[DataProvider('memoryStopProfiles')] + public function testMemoryLimitStopHandsCommittedWorkToAFreshWorker(bool $enabled): void + { + ContinuationProcessEnvironment::configure($this->app, $this->directory . '/database.sqlite', $enabled); + $this->deadline = hrtime(true) + 30_000_000_000; + $stub = WorkflowStub::make(TestContinuationProcessWorkflow::class, 'memory-stop', 'process-loss'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90), $this->directory); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')]; + $stopped = $this->worker($enabled, '', 5, memory: 1); + $stopped->wait(); + $this->assertSame(0, $stopped->getExitCode(), $this->diagnostic($stopped)); + $report = json_decode(file_get_contents($this->directory . '/worker-0.json'), true, flags: JSON_THROW_ON_ERROR); + $this->assertSame(12, $report['command_exit']); + $this->assertSame($enabled ? 1 : 0, $report['peak_entries']); + $this->assertSame(0, $report['cache']['entries']); + $this->assertSame($enabled ? 1 : 0, $report['cache']['counts']['evict_clear'] ?? 0); + $this->assertSame(0, $report['transaction_level']); + $this->assertFalse($report['pdo_transaction']); + $this->assertCount(1, file($this->directory . '/entries')); + $this->assertFileDoesNotExist($this->directory . '/effects'); + $prefix = $run->historyEvents() + ->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(); + $this->assertSame(0, $run->tasks()->where('status', 'leased')->count()); + + $successor = $this->worker($enabled, '', 4); + $successor->wait(); + $this->assertSame(0, $successor->getExitCode(), $this->diagnostic($successor)); + $this->assertSame('completed', $stub->refresh()->status()); + $this->assertSame([ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ], $stub->output()); + $this->assertCount($enabled ? 2 : 3, file($this->directory . '/entries')); + $this->assertCount(1, file($this->directory . '/effects')); + $events = $run->historyEvents() + ->orderBy('sequence') + ->get(); + $this->assertSame($prefix, $events->take(count($prefix))->map->getRawOriginal()->all()); + $this->assertCount(1, $events->where('event_type', HistoryEventType::ActivityCompleted)); + $this->assertCount(1, $events->where('event_type', HistoryEventType::TimerFired)); + $this->assertCount(1, $events->where('event_type', HistoryEventType::WorkflowCompleted)); + $run->refresh(); + $this->assertSame($deadlines, [$run->getRawOriginal('execution_deadline_at'), + $run->getRawOriginal('run_deadline_at')]); + $tasks = $run->tasks() + ->get(); + $this->assertCount(5, $tasks); + $this->assertSame(['completed'], $tasks->pluck('status')->map->value->unique()->values()->all()); + $this->assertSame([1], $tasks->pluck('attempt_count')->unique()->values()->all()); + $this->assertSame(0, $tasks->sum('repair_count')); + $this->assertSame(0, DB::table('failed_jobs')->count()); + $this->assertSame(0, DB::table('jobs')->count()); + } + + #[DataProvider('profiles')] + public function testKilledWorkerRecoversTheSameClaimWithoutRepeatingCommittedEffects( + bool $enabled, + int $entriesBeforeKill, + int $entriesAfterRepair, + ): void { + if (PHP_OS_FAMILY !== 'Linux') { + $this->markTestSkipped('This process-loss control requires Linux SIGKILL.'); + } + ContinuationProcessEnvironment::configure($this->app, $this->directory . '/database.sqlite', $enabled); + $maintenance = new ContinuationTaskMaintenance(); + WorkflowTask::updated($maintenance->observe(...)); + $this->deadline = hrtime(true) + 30_000_000_000; + $stub = WorkflowStub::make(TestContinuationProcessWorkflow::class, 'process-loss', 'process-loss'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90), $this->directory); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')]; + $checkpoint = $this->directory . '/checkpoint'; + $original = $this->worker($enabled, $checkpoint, 5); + while (! is_file($checkpoint) && $original->isRunning()) { + $this->remaining(); + $original->checkTimeout(); + usleep(10_000); + } + $this->assertFileExists($checkpoint, $this->diagnostic($original)); + $claim = json_decode(file_get_contents($checkpoint), true, flags: JSON_THROW_ON_ERROR); + $this->assertSame($original->getPid(), $claim['pid']); + $this->assertSame(0, $claim['transaction_level']); + $this->assertFalse($claim['pdo_transaction']); + $this->assertSame($enabled ? 1 : 0, $claim['cache']['entries']); + if ($enabled) { + $this->assertGreaterThanOrEqual(1, $claim['cache']['counts']['hit'] ?? 0); + } + $this->assertCount($entriesBeforeKill, file($this->directory . '/entries')); + $this->assertCount(1, file($this->directory . '/effects')); + $task = WorkflowTask::query()->findOrFail($claim['task_id']); + $this->assertSame('leased', $task->status->value); + $this->assertSame(1, $task->attempt_count); + $this->assertSame($claim['lease'], $task->getRawOriginal('lease_expires_at')); + $prefix = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id) + ->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(); + + $original->signal(9); + while ($original->isRunning()) { + $this->remaining(); + usleep(10_000); + } + $this->assertSame(9, $original->getTermSignal()); + while (now()->lt($task->lease_expires_at)) { + $this->remaining(); + usleep(10_000); + } + $this->assertSame($claim['lease'], $task->fresh()->getRawOriginal('lease_expires_at')); + do { + $this->remaining(); + $repair = TaskWatchdog::runPass(runIds: [$run->id]); + $this->assertSame([], $repair['existing_task_failures']); + if ($repair['repaired_existing_tasks'] === 0) { + usleep(100_000); + } + } while ($repair['repaired_existing_tasks'] === 0); + $this->assertSame(1, $repair['repaired_existing_tasks'], json_encode([ + 'report' => $repair, + 'task' => $task->fresh() + ->getAttributes(), + 'now' => now() + ->format('Y-m-d H:i:s.u'), + 'jobs' => DB::table('jobs')->get()->map(static fn ($job): array => [ + 'id' => $job->id, + 'attempts' => $job->attempts, + 'reserved_at' => $job->reserved_at, + 'available_at' => $job->available_at, + ])->all(), + ], JSON_THROW_ON_ERROR)); + $this->assertSame([ + 'ready_redispatches' => 0, + 'lease_recoveries' => 1, + 'other_repairs' => 0, + ], $maintenance->snapshot()); + $this->assertSame(1, $task->fresh()->repair_count); + $successor = $this->worker($enabled, '', 2); + $successor->wait(); + $this->assertSame(0, $successor->getExitCode(), $this->diagnostic($successor)); + $this->assertSame('completed', $stub->refresh()->status()); + $this->assertSame([ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ], $stub->output()); + $this->assertCount($entriesAfterRepair, file($this->directory . '/entries')); + $this->assertCount(1, file($this->directory . '/effects')); + $this->assertSame($deadlines, [ + $run->fresh() + ->getRawOriginal('execution_deadline_at'), + $run->fresh() + ->getRawOriginal('run_deadline_at'), + ]); + $events = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id)->orderBy('sequence')->get(); + $this->assertSame($prefix, $events->take(count($prefix))->map->getRawOriginal()->all()); + $this->assertCount(1, $events->where('event_type', HistoryEventType::ActivityCompleted)); + $this->assertCount(1, $events->where('event_type', HistoryEventType::WorkflowCompleted)); + $tasks = WorkflowTask::query()->where('workflow_run_id', $run->id)->get(); + $this->assertCount(5, $tasks); + $this->assertSame(['completed'], $tasks->pluck('status')->map->value->unique()->values()->all()); + $this->assertSame(2, $task->fresh()->attempt_count); + $this->assertSame(1, $tasks->sum('repair_count')); + $this->assertSame(0, DB::table('jobs')->count()); + // The killed delivery's declared tries=1 refuses its broker retry after recovery. + $this->assertSame(1, DB::table('failed_jobs')->count()); + $this->assertStringContainsString($task->id, DB::table('failed_jobs')->sole()->payload); + $this->assertStringContainsString('MaxAttemptsExceededException', DB::table('failed_jobs')->sole()->exception); + } + + protected function getPackageProviders($app): array + { + return ContinuationProcessEnvironment::providers(); + } + + protected function defineEnvironment($app): void + { + ContinuationProcessEnvironment::configure($app, $this->directory . '/database.sqlite', false); + } + + private function worker(bool $enabled, string $checkpoint, int $jobs, int $memory = 256): Process + { + $index = count($this->workers); + $worker = new Process([ + PHP_BINARY, dirname(__DIR__, 3) . '/vendor/bin/phpunit', + dirname(__DIR__, 2) . '/Support/ContinuationProcessWorkerTest.php', '--colors=never', + ], dirname(__DIR__, 3), [ + 'DWF_PROCESS_DATABASE' => $this->directory . '/database.sqlite', + 'DWF_PROCESS_ENABLED' => $enabled ? '1' : '0', + 'DWF_PROCESS_CHECKPOINT' => $checkpoint, + 'DWF_PROCESS_MAX_JOBS' => (string) $jobs, + 'DWF_PROCESS_MEMORY' => (string) $memory, + 'DWF_PROCESS_REPORT' => $this->directory . '/worker-' . $index . '.json', + 'APP_SERVICES_CACHE' => $this->directory . '/services-' . $index . '.php', + 'APP_PACKAGES_CACHE' => $this->directory . '/packages-' . $index . '.php', + 'XDEBUG_MODE' => 'off', + ], timeout: $this->remaining()); + $this->workers[] = $worker; + $worker->start(); + + return $worker; + } + + private function remaining(): float + { + $remaining = ($this->deadline - hrtime(true)) / 1_000_000_000; + $this->assertGreaterThan(0, $remaining, 'The original 30-second process-loss observation expired.'); + + return $remaining; + } + + private function diagnostic(Process $worker): string + { + return substr($worker->getOutput() . $worker->getErrorOutput(), -8192); + } +} diff --git a/tests/Unit/V2/EmbeddedContinuationTest.php b/tests/Unit/V2/EmbeddedContinuationTest.php new file mode 100644 index 000000000..cb9411b75 --- /dev/null +++ b/tests/Unit/V2/EmbeddedContinuationTest.php @@ -0,0 +1,1538 @@ +path = tempnam(sys_get_temp_dir(), 'dw-continuation-'); + parent::setUp(); + Carbon::setTestNow('2026-10-17 15:15:00.123456'); + $this->artisan('migrate', [ + '--force' => true, + ])->run(); + DB::statement('CREATE TABLE jobs (id INTEGER PRIMARY KEY AUTOINCREMENT, + queue TEXT NOT NULL, payload TEXT NOT NULL, attempts INTEGER NOT NULL, + reserved_at INTEGER, available_at INTEGER NOT NULL, created_at INTEGER NOT NULL)'); + DB::statement('CREATE TABLE failed_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL UNIQUE, connection TEXT NOT NULL, queue TEXT NOT NULL, + payload TEXT NOT NULL, exception TEXT NOT NULL, failed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)'); + ContinuationWorkflow::$entries = 0; + ContinuationWorkflow::$afterActivity = 0; + ContinuationActivity::$calls = 0; + } + + protected function tearDown(): void + { + Carbon::setTestNow(); + parent::tearDown(); + foreach (['', '-wal', '-shm'] as $suffix) { + if (is_file($this->path . $suffix)) { + unlink($this->path . $suffix); + } + } + } + + public static function profiles(): array + { + return [ + 'ordinary cold jobs' => [false, 3, 2, 'none'], + 'retained jobs' => [true, 1, 1, 'none'], + 'lost process-local state' => [true, 2, 1, 'evict'], + 'new application build' => [true, 2, 1, 'build'], + 'reconnected database session' => [true, 2, 1, 'reconnect'], + 'changed history model' => [true, 2, 1, 'history_event_model'], + 'changed activity model' => [true, 2, 1, 'activity_execution_model'], + 'changed timer model' => [true, 2, 1, 'timer_model'], + 'changed task model' => [true, 2, 1, 'task_model'], + 'ready task transport redispatch' => [true, 1, 1, 'redispatch'], + 'uncertain broker acknowledgement' => [true, 2, 2, 'broker_ack'], + 'duplicate completed delivery' => [true, 1, 1, 'duplicate'], + 'worker stop' => [true, 2, 1, 'stop'], + 'container binding stays cold' => [true, 3, 2, 'binding'], + ]; + } + + #[DataProvider('profiles')] + public function testActualDatabaseJobsKeepCommittedEffectsAndOriginalDeadlines( + bool $enabled, + int $entries, + int $afterActivity, + string $change, + ): void { + $maintenance = new ContinuationTaskMaintenance(); + WorkflowTask::updated($maintenance->observe(...)); + $heldDelivery = null; + $brokerFault = false; + $entriesAtBrokerFault = null; + $brokerFailures = []; + config() + ->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [ContinuationWorkflow::class] : [], + 'build' => 'continuation-test-build', + ]); + if ($change === 'binding') { + $this->app->bind(ContinuationWorkflow::class, static fn () => new ContinuationWorkflow()); + } + $stub = WorkflowStub::make(ContinuationWorkflow::class, 'original', 'continuation-test'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90)); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')]; + $this->workOnce(); // workflow parks on the activity + if ($change === 'duplicate') { + Queue::connection('database')->push(new RunWorkflowTask( + WorkflowTask::query()->where('workflow_run_id', $run->id)->where('task_type', 'workflow')->sole()->id, + )); + } + $prefix = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id) + ->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(); + $this->workOnce(); // actual activity callback + if ($change === 'duplicate') { + $this->workOnce(); + $this->assertSame(1, ContinuationWorkflow::$entries); + } + if ($change === 'evict') { + $this->app->make(WorkflowContinuationCache::class)->clear(); + } elseif ($change === 'build') { + config()->set('workflows.v2.live_execution.build', 'new-build'); + } elseif ($change === 'stop') { + $this->app->make('queue.worker') + ->stop(); + } elseif ($change === 'reconnect') { + DB::disconnect('continuation'); + } elseif (in_array( + $change, + ['history_event_model', 'activity_execution_model', 'timer_model', 'task_model'], + true + )) { + $model = match ($change) { + 'history_event_model' => ContinuationConfiguredHistory::class, + 'activity_execution_model' => ContinuationConfiguredActivity::class, + 'timer_model' => ContinuationConfiguredTimer::class, + 'task_model' => ContinuationConfiguredTask::class, + }; + config() + ->set('workflows.v2.' . $change, $model); + } elseif ($change === 'broker_ack') { + $deliveryId = DB::table('jobs')->sole()->id; + $brokerFault = true; + $this->app['events']->listen(JobFailed::class, static function (JobFailed $event) use ( + &$brokerFailures + ): void { + $brokerFailures[] = [(string) $event->job->getJobId(), $event->exception::class]; + }); + DB::connection()->beforeExecuting(static function (string $sql, array $bindings) use ( + $deliveryId, + &$brokerFault, + &$entriesAtBrokerFault + ): void { + if ($brokerFault && str_starts_with($sql, 'delete from "jobs"') + && (string) ($bindings[0] ?? '') === (string) $deliveryId) { + $brokerFault = false; + $entriesAtBrokerFault = app(WorkflowContinuationCache::class)->snapshot()['entries']; + throw new RuntimeException('controlled-broker-ack'); + } + }); + } elseif ($change === 'redispatch') { + // Reserve through the real broker, then leave the logical task unclaimed. + $heldDelivery = Queue::connection('database')->pop('default'); + $this->assertInstanceOf(DatabaseJob::class, $heldDelivery); + Carbon::setTestNow(now()->addSeconds(TaskRepairPolicy::redispatchAfterSeconds() + 1)); + $repair = TaskWatchdog::runPass(runIds: [$run->id]); + $this->assertSame(1, $repair['repaired_existing_tasks'], json_encode([ + 'repair' => $repair, + 'now' => now() + ->toJSON(), + 'tasks' => $run->tasks() + ->get() + ->map(static fn ($task): array => [ + ...$task->only(['task_type', 'status', 'available_at', 'created_at', 'last_dispatched_at']), + 'needs_redispatch' => TaskRepairPolicy::readyTaskNeedsRedispatch($task), + ])->all(), + ], JSON_THROW_ON_ERROR)); + } + $this->workOnce(); // workflow resumes, then parks on its timer + if ($change === 'broker_ack') { + $this->assertFalse($brokerFault); + $this->assertSame(1, $entriesAtBrokerFault); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->assertSame(1, app(WorkflowContinuationCache::class)->snapshot()['counts']['evict_job_exception']); + } + if ($heldDelivery instanceof DatabaseJob) { + $this->app->make('queue.worker') + ->process( + 'database', + $heldDelivery, + new WorkerOptions(sleep: 0, timeout: 30), + ); // The held obsolete delivery observes the completed logical task. + } + Carbon::setTestNow(now()->addSeconds(2)); + $this->workOnce(); // durable timer fires through the ordinary queue + $this->workOnce(); // final workflow continuation + if ($change === 'broker_ack') { + // The failed delete marked only the in-memory delivery deleted. Let + // the broker expire its real reservation after the workflow finishes. + $this->assertSame('completed', $stub->refresh()->status()); + $terminalHistory = $run->historyEvents() + ->get() + ->map->getRawOriginal() + ->all(); + $obsolete = DB::table('jobs')->sole(); + $this->assertSame($deliveryId, $obsolete->id); + $this->assertNotNull($obsolete->reserved_at); + Carbon::setTestNow(now()->addSeconds((int) config('queue.connections.database.retry_after') + 1)); + $this->workOnce(); // Its original tries=1 rejects the obsolete broker retry. + $this->assertSame($terminalHistory, $run->historyEvents()->get()->map->getRawOriginal()->all()); + $this->assertSame([[(string) $deliveryId, MaxAttemptsExceededException::class]], $brokerFailures); + } + $this->assertSame('completed', $stub->refresh()->status(), Artisan::output()); + $this->assertSame([ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ], $stub->output()); + $this->assertSame(1, ContinuationActivity::$calls); + $this->assertSame($entries, ContinuationWorkflow::$entries); + $this->assertSame($afterActivity, ContinuationWorkflow::$afterActivity); + $this->assertSame($deadlines, [ + $run->fresh() + ->getRawOriginal('execution_deadline_at'), + $run->fresh() + ->getRawOriginal('run_deadline_at'), + ]); + $events = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id)->orderBy('sequence')->get(); + $this->assertSame($prefix, $events->take(count($prefix))->map->getRawOriginal()->all()); + foreach ([HistoryEventType::ActivityScheduled, HistoryEventType::ActivityStarted, + HistoryEventType::ActivityCompleted, HistoryEventType::TimerScheduled, + HistoryEventType::TimerFired, HistoryEventType::WorkflowCompleted] as $type) { + $this->assertCount(1, $events->where('event_type', $type)); + } + $tasks = WorkflowTask::query()->where('workflow_run_id', $run->id)->get(); + $this->assertCount(5, $tasks); + $this->assertSame(['completed'], $tasks->pluck('status')->map->value->unique()->values()->all()); + $this->assertSame([1], $tasks->pluck('attempt_count')->unique()->values()->all()); + $this->assertSame(0, DB::table('failed_jobs')->count()); + $this->assertSame(0, DB::table('jobs')->count()); + $this->assertSame([ + 'ready_redispatches' => $change === 'redispatch' ? 1 : 0, + 'lease_recoveries' => 0, + 'other_repairs' => 0, + ], $maintenance->snapshot()); + $counts = app(WorkflowContinuationCache::class)->snapshot()['counts']; + if ($enabled) { + $this->assertSame($entries, $counts['cold_entry']); + if ($change === 'binding') { + $this->assertSame(0, $counts['validated_history_events'] ?? 0); + } else { + $this->assertGreaterThan(0, $counts['validated_history_events'] ?? 0); + } + } else { + $this->assertArrayNotHasKey('cold_entry', $counts); + } + } + + public static function replacedClaims(): array + { + return [ + 'different owner' => [false], + 'same owner advanced attempt' => [true], + ]; + } + + #[DataProvider('replacedClaims')] + public function testFreshLeaseOwnerIsCheckedBeforeAnyRetainedPrefixCanResume(bool $advancedAttempt): void + { + $this->enable(); + $stub = WorkflowStub::make(ContinuationWorkflow::class, 'lease-original', 'continuation-test'); + $stub->start(); + $this->workOnce(); + $this->workOnce(); + $task = WorkflowTask::query()->where('workflow_run_id', $stub->runId()) + ->where('task_type', 'workflow') + ->where('status', 'ready') + ->sole(); + $writer = new PDO('sqlite:' . $this->path); + $expectedOwner = $advancedAttempt ? $task->id : 'replacement-owner'; + $admissions = 0; + DB::connection()->beforeExecuting(static function (string $sql) use ( + $writer, + $task, + $advancedAttempt, + $expectedOwner, + &$admissions + ): void { + if (str_starts_with($sql, 'update "workflow_tasks" set "id" =') && ++$admissions === 2) { + $statement = $writer->prepare($advancedAttempt + ? 'UPDATE workflow_tasks SET attempt_count = attempt_count + 1, lease_owner = ? WHERE id = ?' + : 'UPDATE workflow_tasks SET lease_owner = ? WHERE id = ?'); + $statement->execute([$expectedOwner, $task->id]); + } + }); + $prefix = WorkflowHistoryEvent::query()->where('workflow_run_id', $stub->runId()) + ->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(); + $this->workOnce(); + $this->assertSame(2, $admissions); + $this->assertSame($expectedOwner, $task->fresh()->lease_owner); + $this->assertSame($advancedAttempt ? 2 : 1, (int) $task->fresh()->attempt_count); + $this->assertSame('leased', $task->fresh()->status->value); + $this->assertSame(1, ContinuationWorkflow::$entries); + $this->assertSame(0, ContinuationWorkflow::$afterActivity); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->assertSame($prefix, WorkflowHistoryEvent::query()->where('workflow_run_id', $stub->runId()) + ->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all()); + } + + public function testOnePersistentDaemonKeepsItsFrameAcrossARealOneSecondTimer(): void + { + $this->enable(); + Carbon::setTestNow(); + $stub = WorkflowStub::make(ContinuationWorkflow::class, 'daemon-original', 'continuation-test'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90)); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')]; + $started = hrtime(true); + $status = Artisan::call('queue:work', [ + 'connection' => 'database', + '--queue' => 'default', + '--max-jobs' => 5, + '--max-time' => 30, + '--sleep' => 1, + '--tries' => 1, + '--timeout' => 30, + ]); + $elapsed = (hrtime(true) - $started) / 1_000_000_000; + $this->assertSame(0, $status, Artisan::output()); + $this->assertSame('completed', $stub->refresh()->status(), Artisan::output()); + $this->assertSame(1, ContinuationWorkflow::$entries); + $this->assertSame(1, ContinuationWorkflow::$afterActivity); + $this->assertSame(1, ContinuationActivity::$calls); + $this->assertSame(5, $run->tasks()->where('status', 'completed')->where('attempt_count', 1)->count()); + $this->assertSame($deadlines, [$run->fresh()->getRawOriginal('execution_deadline_at'), + $run->fresh() + ->getRawOriginal('run_deadline_at')]); + $this->assertGreaterThanOrEqual(1, $elapsed); + $this->assertLessThan(30, $elapsed); + $this->assertSame(0, $this->app->make(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->assertSame(0, DB::table('failed_jobs')->count()); + } + + public function testUnsupportedMemoFallsBackBeforeItsDurableWrite(): void + { + config()->set('workflows.v2.live_execution', [ + 'workflows' => [ContinuationMemoWorkflow::class], + 'build' => 'memo-fallback', + ]); + ContinuationMemoWorkflow::$entries = 0; + $atWrite = []; + WorkflowHistoryEvent::creating(static function (WorkflowHistoryEvent $event) use (&$atWrite): void { + if ($event->event_type === HistoryEventType::MemoUpserted) { + $atWrite[] = ContinuationMemoWorkflow::$entries; + } + }); + $stub = WorkflowStub::make(ContinuationMemoWorkflow::class, 'memo-fallback', 'continuation-test'); + $stub->start(); + $this->workOnce(); + $this->workOnce(); + $this->workOnce(); + $this->assertSame('completed', $stub->refresh()->status()); + $this->assertSame([2], $atWrite); + $this->assertSame(1, ContinuationActivity::$calls); + $this->assertSame(2, app(WorkflowContinuationCache::class)->snapshot()['counts']['cold_entry']); + $this->assertSame(0, $this->app->make(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->assertSame(1, WorkflowHistoryEvent::query()->where('workflow_run_id', $stub->runId()) + ->where('event_type', HistoryEventType::MemoUpserted)->count()); + } + + public function testRunAndNamespaceStateStayBoundToTheirOwnContinuationAndEventClocks(): void + { + config()->set('workflows.v2.live_execution', [ + 'workflows' => [ScopedContinuationWorkflow::class], + 'build' => 'namespace-test', + ]); + ScopedContinuationWorkflow::$entries = []; + $stubs = []; + foreach (['17', '017'] as $namespace) { + $stub = WorkflowStub::make(ScopedContinuationWorkflow::class, 'scope-' . $namespace, $namespace); + $stub->start($namespace); + $stubs[] = $stub; + } + foreach ([0, 1, 2] as $phase) { + if ($phase === 1) { + Carbon::setTestNow(now()->addMicroseconds(13)); + } + $this->workOnce(); + $this->workOnce(); + } + Carbon::setTestNow(now()->addSeconds(2)); + for ($job = 0; $job < 4; ++$job) { + $this->workOnce(); + } + foreach ($stubs as $stub) { + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $this->assertSame('completed', $stub->refresh()->status()); + $result = $stub->output(); + $this->assertSame($run->namespace, $result['tag']); + $this->assertSame($run->namespace, $result['namespace']); + $this->assertSame($run->id, $result['run']); + $events = $run->historyEvents() + ->get(); + $timer = $events->where('event_type', HistoryEventType::TimerFired)->sole(); + $activity = $events->where('event_type', HistoryEventType::ActivityCompleted)->sole(); + $this->assertSame($timer->sequence, $result['history_at_return']); + $this->assertSame([$run->started_at->toJSON(), $activity->recorded_at->toJSON(), + $timer->recorded_at->toJSON()], $result['clocks']); + $this->assertSame(1, ScopedContinuationWorkflow::$entries[$run->id]); + $this->assertSame(5, $run->tasks()->where('status', 'completed')->where('attempt_count', 1)->count()); + } + $this->assertSame(2, ContinuationActivity::$calls); + } + + public static function failures(): array + { + return [ + 'root rollback' => ['rollback'], + 'lost post-commit acknowledgement' => ['ack'], + ]; + } + + #[DataProvider('failures')] + public function testAdvancedFrameIsDiscardedAfterTransactionFailure(string $phase): void + { + $this->enable(); + $stub = WorkflowStub::make(ContinuationWorkflow::class, 'failed-original', 'continuation-test'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90)); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')]; + $this->workOnce(); + $this->workOnce(); + $inject = true; + WorkflowHistoryEvent::created(static function (WorkflowHistoryEvent $event) use (&$inject, $phase): void { + if ($inject && $event->event_type === HistoryEventType::TimerScheduled) { + $inject = false; + $fault = static fn () => throw new RuntimeException('controlled-' . $phase); + if ($phase === 'ack') { + DB::afterCommit($fault); + } else { + $fault(); + } + } + }); + $this->workOnce(); + $this->assertFalse($inject); + $this->assertSame(0, $this->app->make(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->assertSame(1, ContinuationWorkflow::$entries); + $this->assertSame(1, ContinuationWorkflow::$afterActivity); + $this->assertSame(1, ContinuationActivity::$calls); + $events = WorkflowHistoryEvent::query()->where('workflow_run_id', $stub->runId())->orderBy('sequence')->get(); + $this->assertCount($phase === 'ack' ? 1 : 0, $events->where('event_type', HistoryEventType::TimerScheduled)); + $this->assertCount(1, $events->where('event_type', HistoryEventType::ActivityCompleted)); + $this->assertCount(0, $events->where('event_type', HistoryEventType::WorkflowCompleted)); + $failed = WorkflowTask::query()->where('workflow_run_id', $stub->runId())->where('status', 'failed')->sole(); + $this->assertSame('controlled-' . $phase, $failed->last_error); + $failedState = $failed->getRawOriginal(); + $prefix = $events->map->getRawOriginal() + ->all(); + $failedJobs = DB::table('failed_jobs')->count(); + + $this->assertTrue($stub->refresh()->attemptRepair()->accepted()); + $this->workOnce(); + // A lost acknowledgement can precede the timer's transport dispatch. + // Preserve the ordinary stale-dispatch policy before asking the watchdog. + Carbon::setTestNow(now()->addSeconds(TaskRepairPolicy::redispatchAfterSeconds() + 2)); + $repair = TaskWatchdog::runPass(runIds: [$run->id]); + for ($delivery = 0; $delivery < 10 && DB::table('jobs')->exists(); ++$delivery) { + $this->workOnce(); + } + + $this->assertTrue($stub->refresh()->completed(), json_encode([ + 'repair' => $repair, + 'status' => $run->fresh() +->status->value, + 'now' => now() + ->toJSON(), + 'tasks' => $run->tasks() + ->get() + ->map(static fn ($task): array => $task->only([ + 'id', 'task_type', 'status', 'available_at', 'last_dispatched_at', 'last_error', 'repair_count', + ]))->all(), + 'queued' => DB::table('jobs')->count(), + ], JSON_THROW_ON_ERROR)); + $this->assertSame($failedState, $failed->fresh()->getRawOriginal()); + $this->assertSame(1, ContinuationActivity::$calls); + // RepairRequested keeps recovery cold. Rollback also needs a workflow + // task to reschedule the timer that never committed. + $this->assertSame($phase === 'rollback' ? 3 : 2, ContinuationWorkflow::$entries); + $this->assertSame($deadlines, [$run->fresh()->getRawOriginal('execution_deadline_at'), + $run->fresh() + ->getRawOriginal('run_deadline_at')]); + $events = $run->historyEvents() + ->orderBy('sequence') + ->get(); + $this->assertCount(1, $events->where('event_type', HistoryEventType::RepairRequested)); + $this->assertSame($prefix, $events->take(count($prefix))->map->getRawOriginal()->all()); + $this->assertCount(1, $events->where('event_type', HistoryEventType::TimerScheduled)); + $this->assertCount(1, $events->where('event_type', HistoryEventType::WorkflowCompleted)); + $this->assertSame($failedJobs, DB::table('failed_jobs')->count()); + $this->assertSame(0, DB::table('jobs')->count()); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + } + + public static function modelProfiles(): array + { + return [ + 'ordinary cold' => [false], + 'retained' => [true], + ]; + } + + #[DataProvider('modelProfiles')] + public function testIncompatibleEntryAfterSuspensionPreservesTheOriginalForRepair(bool $enabled): void + { + $maintenance = new ContinuationTaskMaintenance(); + WorkflowTask::updated($maintenance->observe(...)); + config() + ->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [ContinuationEntryWorkflow::class] : [], + 'build' => 'entry-binding', + ]); + $stub = WorkflowStub::make(ContinuationEntryWorkflow::class, 'entry-binding', 'continuation-test'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90), 7); + $this->workOnce(); + $cache = app(WorkflowContinuationCache::class); + $this->assertSame($enabled ? 1 : 0, $cache->snapshot()['entries']); + Carbon::setTestNow(now()->addSeconds(2)); + $this->workOnce(); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $original = array_intersect_key($run->getRawOriginal(), array_flip([ + 'id', 'workflow_instance_id', 'workflow_class', 'workflow_type', 'arguments', 'payload_codec', + 'started_at', 'execution_deadline_at', 'run_deadline_at', + ])); + $prefix = $run->historyEvents() + ->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(); + $task = $run->tasks() + ->where('task_type', 'workflow') + ->where('status', 'ready') + ->sole(); + $this->app->bind(ContinuationEntryWorkflow::class, static fn () => new ContinuationIncompatibleEntry()); + $this->workOnce(); + + $this->assertFalse($run->fresh()->status->isTerminal()); + $this->assertSame($original, array_intersect_key($run->fresh()->getRawOriginal(), $original)); + $this->assertSame($prefix, $run->historyEvents()->orderBy('sequence')->get()->map->getRawOriginal()->all()); + $this->assertSame('failed', $task->fresh()->status->value); + $this->assertSame('workflow_entry_arguments_mismatch', $task->fresh()->payload['replay_blocked_reason']); + $this->assertSame(0, $cache->snapshot()['entries']); + $this->assertSame(0, ContinuationIncompatibleEntry::$entries); + $this->assertSame(0, DB::table('failed_jobs')->count()); + + $this->app->offsetUnset(ContinuationEntryWorkflow::class); + $this->assertTrue($stub->refresh()->attemptRepair()->accepted()); + $this->workOnce(); + $this->assertTrue($stub->refresh()->completed(), Artisan::output()); + $this->assertSame(7, $stub->output()); + $this->assertSame('completed', $task->fresh()->status->value); + $this->assertSame($original, array_intersect_key($run->fresh()->getRawOriginal(), $original)); + $this->assertSame($prefix, $run->historyEvents()->orderBy('sequence')->get()->take(count($prefix)) + ->map->getRawOriginal() + ->all()); + $this->assertSame(1, $run->historyEvents()->where('event_type', 'TimerScheduled')->count()); + $this->assertSame(0, $run->historyEvents()->where('event_type', 'WorkflowFailed')->count()); + $this->assertSame([ + 'ready_redispatches' => 0, + 'lease_recoveries' => 0, + 'other_repairs' => 1, + ], $maintenance->snapshot()); + } + + public static function bodyFailures(): iterable + { + foreach ([false, true] as $enabled) { + foreach (['type-error', 'internal-mismatch'] as $failure) { + yield ($enabled ? 'retained-' : 'cold-') . $failure => [$enabled, $failure]; + } + } + } + + #[DataProvider('bodyFailures')] + public function testAValidEntryBodyFailureAfterSuspensionRemainsAnApplicationFailure( + bool $enabled, + string $failure, + ): void { + config()->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [ContinuationEntryBodyFailure::class] : [], + 'build' => 'entry-body-failure', + ]); + ContinuationEntryBodyFailure::$entries = 0; + $stub = WorkflowStub::make(ContinuationEntryBodyFailure::class, 'entry-body', 'continuation-test'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90), $failure); + $this->workOnce(); + $this->assertSame($enabled ? 1 : 0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + Carbon::setTestNow(now()->addSeconds(2)); + $this->workOnce(); + $this->workOnce(); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + + $this->assertTrue($stub->refresh()->failed(), Artisan::output()); + $this->assertSame($enabled ? 1 : 2, ContinuationEntryBodyFailure::$entries); + $this->assertSame(1, $run->historyEvents()->where('event_type', 'WorkflowFailed')->count()); + foreach ($run->tasks()->get() as $task) { + $this->assertFalse($task->payload['replay_blocked'] ?? false); + } + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->assertSame(0, DB::table('failed_jobs')->count()); + } + + #[DataProvider('modelProfiles')] + public function testAConfiguredRunModelChangeUsesFreshRuntimeState(bool $enabled): void + { + ContinuationModelWorkflow::$entries = 0; + config() + ->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [ContinuationModelWorkflow::class] : [], + 'build' => 'unchanged-model-build', + ]); + $stub = WorkflowStub::make(ContinuationModelWorkflow::class, 'model-change', 'continuation-test'); + $stub->start(); + $this->workOnce(); + $this->assertSame($enabled ? 1 : 0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + config() + ->set('workflows.v2.run_model', ContinuationConfiguredRun::class); + $this->workOnce(); + $this->workOnce(); + $this->assertSame('completed', $stub->refresh()->status()); + $this->assertSame(ContinuationConfiguredRun::class, $stub->output()['model']); + $this->assertSame(2, ContinuationModelWorkflow::$entries); + $this->assertSame(1, ContinuationActivity::$calls); + if ($enabled) { + $this->assertSame( + 1, + app(WorkflowContinuationCache::class)->snapshot()['counts']['invalidate_identity'] ?? 0 + ); + } + } + + public static function fallbackOperations(): iterable + { + $operations = [ + 'parallel' => [HistoryEventType::TimerScheduled, [true, true]], + 'selection' => [ + HistoryEventType::TimerScheduled, [ + 'key' => 'left', + 'winner' => true, + 'remaining' => true, + ]], + 'child' => [HistoryEventType::ChildWorkflowScheduled, 'child-value'], + 'condition' => [HistoryEventType::ConditionWaitOpened, true], + 'continue' => [HistoryEventType::WorkflowContinuedAsNew, 'continued'], + 'nexus' => [HistoryEventType::ServiceCallStarted, [ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'Nexus λ', + ]], + ]; + foreach ($operations as $operation => [$event, $result]) { + foreach ([false, true] as $enabled) { + yield $operation . ($enabled ? ' retained fallback' : ' cold') => [ + $enabled, + $operation, + $event, + $result, + ]; + } + } + } + + #[DataProvider('fallbackOperations')] + public function testExcludedOperationsReplayBeforeTheirFirstDurableWrite( + bool $enabled, + string $operation, + HistoryEventType $firstEvent, + mixed $result, + ): void { + ContinuationFallbackWorkflow::$entries = []; + config() + ->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [ContinuationFallbackWorkflow::class] : [], + 'build' => 'excluded-operations', + ]); + $backendStarts = 0; + $serviceRun = $operation === 'nexus' ? $this->registerNexusService($backendStarts) : null; + $stub = WorkflowStub::make(ContinuationFallbackWorkflow::class, 'fallback-' . $operation, 'continuation-test'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90), $operation); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')]; + $atWrite = []; + $atServiceCreate = []; + WorkflowServiceCall::creating(static function (WorkflowServiceCall $call) use ($run, &$atServiceCreate): void { + if ($call->caller_workflow_run_id === $run->id) { + $atServiceCreate[] = ContinuationFallbackWorkflow::$entries[$run->id] ?? 0; + } + }); + WorkflowHistoryEvent::creating(static function (WorkflowHistoryEvent $event) use ( + $run, + $firstEvent, + &$atWrite + ): void { + if ($event->workflow_run_id === $run->id && $event->event_type === $firstEvent) { + $atWrite[] = ContinuationFallbackWorkflow::$entries[$run->id] ?? 0; + } + }); + $this->workOnce(); + $this->assertSame($enabled ? 1 : 0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->workOnce(); + $prefix = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id)->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(); + $this->workOnce(); + $this->assertNotEmpty($atWrite); + $this->assertSame([2], array_values(array_unique($atWrite))); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + if ($enabled) { + $this->assertSame( + 1, + app(WorkflowContinuationCache::class)->snapshot()['counts']['cold_unsupported_operation'] ?? 0 + ); + } + if ($serviceRun !== null) { + $this->assertSame([2], $atServiceCreate); + $this->assertSame(1, $backendStarts); + $call = WorkflowServiceCall::query()->where('caller_workflow_run_id', $run->id)->sole(); + $this->assertSame('started', $call->status); + WorkflowHistoryEvent::record($serviceRun, HistoryEventType::WorkflowCompleted, [ + 'output' => Serializer::serializeWithCodec('avro', $result), + 'payload_codec' => 'avro', + ]); + } + Carbon::setTestNow(now()->addSeconds(2)); + for ($jobs = 0; $jobs < 12 && DB::table('jobs')->exists(); ++$jobs) { + $this->workOnce(); + Carbon::setTestNow(now()->addSeconds(2)); + } + $this->assertSame('completed', $stub->refresh()->status(), Artisan::output()); + $this->assertSame([ + 'value' => [ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ], + 'operation' => $result, + ], $stub->output()); + $this->assertSame(1, ContinuationActivity::$calls); + $events = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id)->orderBy('sequence')->get(); + $this->assertSame($prefix, $events->take(count($prefix))->map->getRawOriginal()->all()); + $this->assertCount(1, $events->where('event_type', HistoryEventType::ActivityCompleted)); + if ($serviceRun !== null) { + $this->assertSame(1, $backendStarts); + $this->assertCount(1, $events->where('event_type', HistoryEventType::ServiceCallStarted)); + $this->assertCount(1, $events->where('event_type', HistoryEventType::ServiceCallCompleted)); + $this->assertSame( + 2, + $events->firstWhere('event_type', HistoryEventType::ServiceCallStarted)->payload['sequence'] + ); + $this->assertSame( + 2, + $events->firstWhere('event_type', HistoryEventType::ServiceCallCompleted)->payload['sequence'] + ); + $call = WorkflowServiceCall::query()->where('caller_workflow_run_id', $run->id)->sole(); + $this->assertSame('completed', $call->status); + } + $this->assertSame($deadlines, [$run->fresh()->getRawOriginal('execution_deadline_at'), + $run->fresh() + ->getRawOriginal('run_deadline_at')]); + $this->assertSame( + ['completed'], + WorkflowTask::query()->get()->pluck('status')->map->value->unique()->values()->all() + ); + $this->assertSame([1], WorkflowTask::query()->pluck('attempt_count')->unique()->values()->all()); + $this->assertSame([0], WorkflowTask::query()->pluck('repair_count')->unique()->values()->all()); + $this->assertSame(0, DB::table('failed_jobs')->count()); + $this->assertSame(0, DB::table('jobs')->count()); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + } + + #[DataProvider('modelProfiles')] + public function testDeclaredQueriesUpdatesAndSignalsKeepTheirColdReplayContract(bool $enabled): void + { + config()->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [TestUpdateWorkflow::class] : [], + 'build' => 'interactive-cold-profile', + ]); + $stub = WorkflowStub::make(TestUpdateWorkflow::class, 'interactive', 'continuation-test'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90)); + $this->workOnce(); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')]; + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $prefix = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id)->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(); + $this->assertSame( + [ + 'stage' => 'waiting-for-name', + 'approved' => false, + 'events' => ['started'], + ], + $stub->query('currentState') + ); + $this->assertSame( + $prefix, + WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id)->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all() + ); + $update = $stub->submitUpdate('approve', true, 'cold-profile'); + $this->assertTrue($update->accepted()); + $this->workOnce(); + $this->assertTrue($stub->inspectUpdate($update->updateId())->completed()); + $this->assertSame([ + 'stage' => 'waiting-for-name', + 'approved' => true, + 'events' => ['started', 'approved:yes:cold-profile'], + ], $stub->query('currentState')); + $this->assertTrue($stub->signal('name-provided', 'λ')->accepted()); + for ($jobs = 0; $jobs < 8 && DB::table('jobs')->exists(); ++$jobs) { + $this->workOnce(); + } + $this->assertSame('completed', $stub->refresh()->status(), Artisan::output()); + $this->assertTrue($stub->output()['approved']); + $this->assertSame(['started', 'approved:yes:cold-profile', 'signal:λ'], $stub->output()['events']); + $events = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id)->orderBy('sequence')->get(); + $this->assertSame($prefix, $events->take(count($prefix))->map->getRawOriginal()->all()); + foreach ([HistoryEventType::UpdateAccepted, HistoryEventType::UpdateCompleted, + HistoryEventType::SignalApplied, HistoryEventType::WorkflowCompleted] as $type) { + $this->assertCount(1, $events->where('event_type', $type)); + } + $this->assertSame($deadlines, [$run->fresh()->getRawOriginal('execution_deadline_at'), + $run->fresh() + ->getRawOriginal('run_deadline_at')]); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->assertSame(0, DB::table('jobs')->count()); + $this->assertSame(0, DB::table('failed_jobs')->count()); + } + + public static function terminalCommands(): iterable + { + foreach ([ + 'cancel' => 'cancelled', + 'terminate' => 'terminated', + ] as $command => $status) { + foreach ([false, true] as $enabled) { + yield $command . ($enabled ? ' retained' : ' cold') => [$enabled, $command, $status]; + } + } + } + + #[DataProvider('terminalCommands')] + public function testExternalTerminalCommandsNeverResumeTheRetainedFrame( + bool $enabled, + string $command, + string $status, + ): void { + config()->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [ContinuationWorkflow::class] : [], + 'build' => 'terminal-authority', + ]); + $stub = WorkflowStub::make(ContinuationWorkflow::class, 'terminal-' . $command, 'continuation-test'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90)); + $this->workOnce(); + $this->workOnce(); + $run = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$run->getRawOriginal('execution_deadline_at'), $run->getRawOriginal('run_deadline_at')]; + $this->assertSame($enabled ? 1 : 0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $prefix = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id)->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(); + $this->assertTrue($stub->{$command}('original terminal command')->accepted()); + for ($jobs = 0; $jobs < 8 && DB::table('jobs')->exists(); ++$jobs) { + $this->workOnce(); + } + $this->assertSame($status, $stub->refresh()->status()); + $this->assertSame(1, ContinuationWorkflow::$entries); + $this->assertSame(0, ContinuationWorkflow::$afterActivity); + $this->assertSame(1, ContinuationActivity::$calls); + $events = WorkflowHistoryEvent::query()->where('workflow_run_id', $run->id)->orderBy('sequence')->get(); + $this->assertSame($prefix, $events->take(count($prefix))->map->getRawOriginal()->all()); + $this->assertCount(0, $events->where('event_type', HistoryEventType::TimerScheduled)); + $this->assertCount(0, $events->where('event_type', HistoryEventType::WorkflowCompleted)); + $this->assertSame($deadlines, [$run->fresh()->getRawOriginal('execution_deadline_at'), + $run->fresh() + ->getRawOriginal('run_deadline_at')]); + $this->app->make('queue.worker') + ->stop(); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->assertSame(0, DB::table('jobs')->count()); + $this->assertSame(0, DB::table('failed_jobs')->count()); + } + + public static function injectedWorkflows(): iterable + { + foreach ([ContinuationConstructorWorkflow::class, ContinuationMethodWorkflow::class] as $class) { + foreach ([false, true] as $enabled) { + yield $class . ($enabled ? ' configured' : ' cold') => [$enabled, $class]; + } + } + } + + public static function broadEntryTypes(): array + { + return [ + 'object' => [ContinuationObjectEntryWorkflow::class], + 'mixed' => [ContinuationMixedEntryWorkflow::class], + 'iterable' => [ContinuationIterableEntryWorkflow::class], + 'callable' => [ContinuationCallableEntryWorkflow::class], + ]; + } + + /** + * @param class-string $class + */ + #[DataProvider('broadEntryTypes')] + public function testObjectCapableBuiltinEntryTypesKeepTheColdLifetime(string $class): void + { + ContinuationBroadEntryWorkflow::$entries = []; + config() + ->set('workflows.v2.live_execution', [ + 'workflows' => [$class], + 'build' => 'object-capable-entry', + ]); + $stub = WorkflowStub::make($class, 'broad-entry', 'continuation-test'); + $stub->start(); + $this->workOnce(); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + Carbon::setTestNow(now()->addSeconds(2)); + $this->workOnce(); + $this->workOnce(); + $this->assertSame('completed', $stub->refresh()->status()); + $this->assertTrue($stub->output()); + $this->assertSame(2, ContinuationBroadEntryWorkflow::$entries[$class]); + $this->assertSame(0, DB::table('failed_jobs')->count()); + $this->assertSame(0, DB::table('jobs')->count()); + } + + /** + * @param class-string $class + */ + #[DataProvider('injectedWorkflows')] + public function testScopedDependenciesAreRebuiltAcrossColdJobBoundaries(bool $enabled, string $class): void + { + config()->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [$class] : [], + 'build' => 'scoped-dependencies', + ]); + config() + ->set('continuation_test.scope', 'first-job'); + $this->app->scoped( + ContinuationScopedDependency::class, + static fn () => new ContinuationScopedDependency(config('continuation_test.scope')) + ); + $stub = WorkflowStub::make($class, 'scoped', 'continuation-test'); + $stub->start(); + $this->workOnce(); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->app->forgetScopedInstances(); + config() + ->set('continuation_test.scope', 'second-job'); + $this->workOnce(); + $this->workOnce(); + $this->assertSame('completed', $stub->refresh()->status(), Artisan::output()); + $this->assertSame([ + 'scope' => 'second-job', + 'value' => + [ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ], + ], $stub->output()); + $this->assertSame(1, ContinuationActivity::$calls); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->assertSame(0, DB::table('jobs')->count()); + $this->assertSame(0, DB::table('failed_jobs')->count()); + if ($enabled) { + $this->assertGreaterThan( + 0, + app(WorkflowContinuationCache::class)->snapshot()['counts']['cold_profile'] ?? 0 + ); + } + } + + #[DataProvider('modelProfiles')] + public function testFailedActivityRedriveDoesNotRepeatTheCommittedPrefix(bool $enabled): void + { + ContinuationRedriveActivity::$calls = 0; + config() + ->set('workflows.v2.live_execution', [ + 'workflows' => $enabled ? [ContinuationRedriveWorkflow::class] : [], + 'build' => 'redrive-original-prefix', + ]); + $stub = WorkflowStub::make(ContinuationRedriveWorkflow::class, 'redrive', 'continuation-test'); + $stub->start(new StartOptions(executionTimeoutSeconds: 120, runTimeoutSeconds: 90)); + $this->workOnce(); + $this->workOnce(); + $this->assertSame($enabled ? 1 : 0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $this->workOnce(); + $this->workOnce(); + $this->workOnce(); + $this->assertSame('failed', $stub->refresh()->status(), Artisan::output()); + $source = WorkflowRun::query()->findOrFail($stub->runId()); + $deadlines = [$source->getRawOriginal('execution_deadline_at'), $source->getRawOriginal('run_deadline_at')]; + $prefix = WorkflowHistoryEvent::query()->where('workflow_run_id', $source->id)->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all(); + $this->assertSame(1, ContinuationActivity::$calls); + $this->assertSame(1, ContinuationRedriveActivity::$calls); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + $controlPlane = app(WorkflowControlPlane::class); + $options = [ + 'namespace' => 'continuation-test', + 'request_id' => 'redrive-once', + ]; + $redrive = $controlPlane->redrive('redrive', $source->id, $options); + $this->assertTrue($redrive['accepted'], (string) $redrive['reason']); + $this->assertNotSame($source->id, $redrive['workflow_run_id']); + $this->assertSame($source->id, $redrive['continued_from_run_id']); + $this->assertSame(2, $redrive['resume_step_sequence']); + $this->assertSame( + $redrive['workflow_run_id'], + $controlPlane->redrive('redrive', $source->id, $options)['workflow_run_id'] + ); + for ($jobs = 0; $jobs < 8 && DB::table('jobs')->exists(); ++$jobs) { + $this->workOnce(); + } + $this->assertSame('completed', $stub->refresh()->status(), Artisan::output()); + $this->assertSame([ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ], $stub->output()); + $this->assertSame(1, ContinuationActivity::$calls); + $this->assertSame(2, ContinuationRedriveActivity::$calls); + $this->assertSame(2, WorkflowRun::query()->count()); + $this->assertSame('failed', $source->fresh()->status->value); + $this->assertSame( + $prefix, + WorkflowHistoryEvent::query()->where('workflow_run_id', $source->id)->orderBy('sequence') + ->get() + ->map->getRawOriginal() + ->all() + ); + $this->assertSame($deadlines, [$source->fresh()->getRawOriginal('execution_deadline_at'), + $source->fresh() + ->getRawOriginal('run_deadline_at')]); + $successor = WorkflowRun::query()->findOrFail($redrive['workflow_run_id']); + $this->assertSame($deadlines[0], $successor->getRawOriginal('execution_deadline_at')); + $this->assertSame([0], $successor->tasks()->pluck('repair_count')->unique()->values()->all()); + $this->assertSame(0, DB::table('jobs')->count()); + $this->assertSame(0, app(WorkflowContinuationCache::class)->snapshot()['entries']); + } + + protected function getPackageProviders($app): array + { + return [TestDatabaseServiceProvider::class, WorkflowServiceProvider::class]; + } + + protected function defineEnvironment($app): void + { + $app['config']->set('database.default', 'continuation'); + $app['config']->set('database.connections.continuation', [ + 'driver' => 'sqlite', + 'database' => $this->path, + 'prefix' => '', + 'foreign_key_constraints' => true, + 'busy_timeout' => 1000, + ]); + $app['config']->set('workflows.storage.connection', 'continuation'); + $app['config']->set('workflows.v1.enabled', false); + $app['config']->set('workflows.v2.task_dispatch_mode', 'queue'); + $app['config']->set('queue.default', 'database'); + $app['config']->set('queue.connections.database', [ + 'driver' => 'database', + 'connection' => 'continuation', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + ]); + $app['config']->set('queue.failed', [ + 'driver' => 'database-uuids', + 'database' => 'continuation', + 'table' => 'failed_jobs', + ]); + $app['config']->set('cache.default', 'array'); + $app['config']->set('logging.default', 'null'); + } + + private function enable(): void + { + config()->set('workflows.v2.live_execution', [ + 'workflows' => [ContinuationWorkflow::class], + 'build' => 'continuation-test-build', + ]); + } + + private function workOnce(): void + { + $this->app->make('queue.worker') + ->runNextJob('database', 'default', new WorkerOptions(sleep: 0, timeout: 30)); + $this->assertSame(0, DB::transactionLevel()); + } + + private function registerNexusService(int &$starts): WorkflowRun + { + $instance = WorkflowInstance::query()->create([ + 'namespace' => 'service', + 'workflow_type' => 'nexus.service', + 'workflow_class' => 'nexus.service', + 'run_count' => 1, + ]); + $run = WorkflowRun::query()->create([ + 'workflow_instance_id' => $instance->id, + 'namespace' => 'service', + 'run_number' => 1, + 'workflow_type' => 'nexus.service', + 'workflow_class' => 'nexus.service', + 'status' => 'waiting', + 'payload_codec' => 'avro', + 'arguments' => Serializer::serializeWithCodec('avro', []), + 'connection' => 'sync', + 'queue' => 'default', + 'started_at' => now(), + ]); + $endpoint = WorkflowServiceEndpoint::query()->create([ + 'namespace' => 'service', + 'endpoint_name' => 'php-service', + ]); + $service = WorkflowService::query()->create([ + 'namespace' => 'service', + 'workflow_service_endpoint_id' => $endpoint->id, + 'service_name' => 'greeter', + ]); + WorkflowServiceOperation::query()->create([ + 'namespace' => 'service', + 'workflow_service_endpoint_id' => $endpoint->id, + 'workflow_service_id' => $service->id, + 'operation_name' => 'greet', + 'operation_mode' => 'async', + 'handler_binding_kind' => 'workflow_run', + 'handler_target_reference' => 'nexus.service', + ]); + $control = Mockery::mock(WorkflowControlPlane::class); + $control->shouldReceive('start') + ->once() + ->andReturnUsing(function () use ($run, &$starts): array { + $this->assertSame([2], array_values(ContinuationFallbackWorkflow::$entries)); + ++$starts; + return [ + 'started' => true, + 'workflow_instance_id' => $run->workflow_instance_id, + 'workflow_run_id' => $run->id, + ]; + }); + $this->app->instance(ServiceControlPlane::class, new DefaultServiceControlPlane($control)); + return $run; + } +} + +#[Type('continuation-workflow')] +final class ContinuationWorkflow extends Workflow +{ + public static int $entries = 0; + + public static int $afterActivity = 0; + + public function handle(): array + { + ++self::$entries; + $value = Workflow::activity(ContinuationActivity::class); + ++self::$afterActivity; + Workflow::timer(1); + + return $value; + } +} + +abstract class ContinuationBroadEntryWorkflow extends Workflow +{ + public static array $entries = []; + + protected function park(mixed $context): bool + { + self::$entries[static::class] = (self::$entries[static::class] ?? 0) + 1; + Workflow::timer(1); + + return $context === null; + } +} + +final class ContinuationObjectEntryWorkflow extends ContinuationBroadEntryWorkflow +{ + public function handle(?object $context = null): bool + { + return $this->park($context); + } +} + +final class ContinuationMixedEntryWorkflow extends ContinuationBroadEntryWorkflow +{ + public function handle(mixed $context = null): bool + { + return $this->park($context); + } +} + +final class ContinuationIterableEntryWorkflow extends ContinuationBroadEntryWorkflow +{ + public function handle(?iterable $context = null): bool + { + return $this->park($context); + } +} + +final class ContinuationCallableEntryWorkflow extends ContinuationBroadEntryWorkflow +{ + public function handle(?callable $context = null): bool + { + return $this->park($context); + } +} + +#[Type('continuation-activity')] +final class ContinuationActivity extends Activity +{ + public static int $calls = 0; + + public function handle(): array + { + ++self::$calls; + + return [ + 'integer' => 9007199254740993, + 'optional' => null, + 'unicode' => 'λ', + ]; + } +} + +final class ContinuationEntryWorkflow extends Workflow +{ + public function handle(int $amount): int + { + Workflow::timer(1); + + return $amount; + } +} + +final class ContinuationIncompatibleEntry extends Workflow +{ + public static int $entries = 0; + + public function handle(string $amount): string + { + ++self::$entries; + Workflow::timer(1); + + return $amount; + } +} + +final class ContinuationEntryBodyFailure extends Workflow +{ + public static int $entries = 0; + + public function handle(string $failure): never + { + ++self::$entries; + Workflow::timer(1); + + if ($failure === 'internal-mismatch') { + throw new WorkflowEntryArgumentsMismatchException(self::class, 'handle', 'application_body_reason'); + } + + throw new \TypeError('Application body failure.'); + } +} + +final class ContinuationRedriveWorkflow extends Workflow +{ + public function handle(): array + { + $value = Workflow::activity(ContinuationActivity::class); + + return Workflow::activity(ContinuationRedriveActivity::class, $value); + } +} + +final class ContinuationRedriveActivity extends Activity +{ + public static int $calls = 0; + + public function handle(array $value): array + { + if (++self::$calls === 1) { + throw new NonRetryableException('Original redrive failure.'); + } + + return $value; + } +} + +final class ContinuationScopedDependency +{ + public function __construct( + public readonly string $scope + ) { + } +} + +final class ContinuationConstructorWorkflow extends Workflow +{ + public function __construct( + private readonly ContinuationScopedDependency $dependency + ) { + } + + public function handle(): array + { + $value = Workflow::activity(ContinuationActivity::class); + + return [ + 'scope' => $this->dependency->scope, + 'value' => $value, + ]; + } +} + +final class ContinuationMethodWorkflow extends Workflow +{ + public function handle(ContinuationScopedDependency $dependency): array + { + $value = Workflow::activity(ContinuationActivity::class); + + return [ + 'scope' => $dependency->scope, + 'value' => $value, + ]; + } +} + +final class ContinuationConfiguredRun extends WorkflowRun +{ +} + +final class ContinuationFallbackWorkflow extends Workflow +{ + /** + * @var array + */ + public static array $entries = []; + + public function handle(string $operation, ?array $continued = null): array + { + self::$entries[$this->run->id] = (self::$entries[$this->run->id] ?? 0) + 1; + if ($continued !== null) { + return $continued; + } + $value = Workflow::activity(ContinuationActivity::class); + $result = match ($operation) { + 'parallel' => Workflow::all([ + static fn () => Workflow::timer(1), + static fn () => Workflow::timer(1), + ]), + 'selection' => $this->selection(), + 'child' => Workflow::child(ContinuationFallbackChild::class), + 'condition' => Workflow::await(static fn (): bool => true, 1, 'fallback-ready'), + 'continue' => Workflow::continueAsNew($operation, [ + 'value' => $value, + 'operation' => 'continued', + ]), + 'nexus' => $this->nexus(), + }; + + return [ + 'value' => $value, + 'operation' => $result, + ]; + } + + /** + * @return array + */ + private function nexus(): array + { + $result = Workflow::serviceOperation('php-service', 'greeter', 'greet', ['world'], new ServiceOperationOptions( + targetNamespace: 'service', + modeOverride: 'sync', + waitFor: 'completed', + )); + if (! $result instanceof ServiceOperationResult || ! is_array($result->responsePayload)) { + throw new RuntimeException('Expected a completed Nexus response.'); + } + return $result->responsePayload; + } + + private function selection(): array + { + $selected = Workflow::select([ + 'left' => static fn () => Workflow::timer(1), + 'right' => static fn () => Workflow::timer(1), + ]); + + return [ + 'key' => $selected->key, + 'winner' => $selected->result(), + 'remaining' => $selected->handles['right']->await(), + ]; + } +} + +final class ContinuationFallbackChild extends Workflow +{ + public function handle(): string + { + return 'child-value'; + } +} + +final class ContinuationModelWorkflow extends Workflow +{ + public static int $entries = 0; + + public function handle(): array + { + ++self::$entries; + Workflow::activity(ContinuationActivity::class); + + return [ + 'model' => get_class($this->run), + ]; + } +} + +final class ContinuationMemoWorkflow extends Workflow +{ + public static int $entries = 0; + + public function handle(): array + { + ++self::$entries; + $value = Workflow::activity(ContinuationActivity::class); + Workflow::upsertMemo([ + 'phase' => 'completed', + ]); + + return $value; + } +} + +final class ScopedContinuationWorkflow extends Workflow +{ + /** + * @var array + */ + public static array $entries = []; + + public function handle(string $tag): array + { + $bound = $this->run; + self::$entries[$bound->id] = (self::$entries[$bound->id] ?? 0) + 1; + $clocks = [Workflow::now()->toJSON()]; + Workflow::activity(ContinuationActivity::class); + $clocks[] = Workflow::now()->toJSON(); + Workflow::timer(1); + $clocks[] = Workflow::now()->toJSON(); + + return [ + 'tag' => $tag, + 'namespace' => $bound->namespace, + 'run' => $bound->id, + 'history_at_return' => $bound->last_history_sequence, + 'clocks' => $clocks, + ]; + } +} + +final class ContinuationConfiguredHistory extends WorkflowHistoryEvent +{ +} + +final class ContinuationConfiguredActivity extends \Workflow\V2\Models\ActivityExecution +{ +} + +final class ContinuationConfiguredTimer extends \Workflow\V2\Models\WorkflowTimer +{ +} + +final class ContinuationConfiguredTask extends WorkflowTask +{ +} diff --git a/tests/Unit/V2/WorkflowContinuationCacheTest.php b/tests/Unit/V2/WorkflowContinuationCacheTest.php new file mode 100644 index 000000000..529f6186a --- /dev/null +++ b/tests/Unit/V2/WorkflowContinuationCacheTest.php @@ -0,0 +1,151 @@ +entry(); + $cache->publish('one', $cache->observe('one'), $entry); + $this->assertSame($entry, $cache->take('one')); + $this->assertNull($cache->take('one')); + $this->assertSame(0, $cache->snapshot()['entries']); + } + + public function testReentrantOlderCommitCannotReplaceANewerGeneration(): void + { + $cache = new WorkflowContinuationCache(); + $older = $cache->observe('one'); + $newer = $cache->observe('one'); + $expected = $this->entry(); + $cache->publish('one', $newer, $expected); + $cache->publish('one', $older, $this->entry()); + $this->assertSame($expected, $cache->take('one')); + $this->assertSame(1, $cache->snapshot()['counts']['discard_publication']); + } + + public function testAChangedBuildDoesNotAuthorizePublicationFromAnOlderRunGeneration(): void + { + $cache = new WorkflowContinuationCache(); + $older = $cache->observe('same-run'); + $newer = $cache->observe('same-run'); + $old = $this->entry(); + $old->scope = 'same-run'; + $current = $this->entry(); + $current->scope = 'same-run'; + $cache->publish('new-build', $newer, $current); + $cache->publish('old-build', $older, $old); + $this->assertNull($cache->take('old-build')); + $this->assertSame($current, $cache->take('new-build')); + } + + public function testCapacityEvictsTheOldestEntryWithoutChangingOtherRuns(): void + { + $cache = new WorkflowContinuationCache(); + for ($index = 0; $index < 5; ++$index) { + $key = 'run-' . $index; + $cache->publish($key, $cache->observe($key), $this->entry()); + } + $this->assertSame(4, $cache->snapshot()['entries']); + $this->assertNull($cache->take('run-0')); + $this->assertNotNull($cache->take('run-1')); + $this->assertNotNull($cache->take('run-4')); + } + + public function testExpiredMonotonicAgeSelectsAMiss(): void + { + $cache = new WorkflowContinuationCache(); + $entry = $this->entry(); + $cache->publish('one', $cache->observe('one'), $entry); + $entry->retainedAt = hrtime(true) - 30_000_000_000; + $this->assertNull($cache->take('one')); + $this->assertSame(1, $cache->snapshot()['counts']['evict_age']); + } + + public function testAHitAndRepublishNeverRenewOriginalAge(): void + { + $cache = new WorkflowContinuationCache(); + $entry = $this->entry(); + $cache->publish('one', $cache->observe('one'), $entry); + $entry->retainedAt = hrtime(true) - 20_000_000_000; + $original = $entry->retainedAt; + $this->assertSame($entry, $cache->take('one')); + $cache->publish('one', $cache->observe('one'), $entry); + $this->assertSame($original, $entry->retainedAt); + $entry->retainedAt = hrtime(true) - 30_000_000_000; + $cache->publish('one', $cache->observe('one'), $entry); + $this->assertNull($cache->take('one')); + $this->assertSame(1, $cache->snapshot()['counts']['discard_age']); + } + + public function testActualResidentPressureEvictsAndRefusesPublication(): void + { + $cache = new WorkflowContinuationCache(); + $cache->publish('one', $cache->observe('one'), $this->entry()); + $this->assertSame(1, $cache->snapshot()['entries']); + $pressure = str_repeat('x', 140 * 1024 * 1024); + $this->assertGreaterThan(128 * 1024 * 1024, $cache->snapshot()['resident_bytes']); + $this->assertSame(0, $cache->snapshot()['entries']); + $cache->publish('two', $cache->observe('two'), $this->entry()); + $this->assertSame(0, $cache->snapshot()['entries']); + $this->assertSame(1, $cache->snapshot()['counts']['evict_memory']); + $this->assertSame(140 * 1024 * 1024, strlen($pressure)); + } + + public function testEvictionCannotResumeDurableCleanupFromFinally(): void + { + $observed = []; + $workflow = new CacheDisposalWorkflow(); + $workflow->run = new WorkflowRun(); + $execution = WorkflowExecution::startCallback(static function () use (&$observed): void { + try { + Workflow::timer(1); + } finally { + $observed[] = 'entered'; + Workflow::timer(1); + $observed[] = 'continued'; + } + }); + $entry = new WorkflowContinuation($workflow, $execution, $execution->current()); + $cache = new WorkflowContinuationCache(); + $cache->publish('one', $cache->observe('one'), $entry); + unset($entry, $execution, $workflow); + $cache->clear(); + gc_collect_cycles(); + $this->assertSame(['entered'], $observed); + $this->assertSame(0, $cache->snapshot()['entries']); + $this->assertSame(1, $cache->snapshot()['counts']['evict_clear']); + } + + private function entry(): WorkflowContinuation + { + $workflow = new CacheDisposalWorkflow(); + $workflow->run = new WorkflowRun(); + $execution = WorkflowExecution::start($workflow, []); + + return new WorkflowContinuation($workflow, $execution, $execution->current()); + } +} + +final class CacheDisposalWorkflow extends Workflow +{ + public function handle(): void + { + Workflow::timer(1); + } +} diff --git a/tests/Unit/V2/WorkflowExecutionProfileTest.php b/tests/Unit/V2/WorkflowExecutionProfileTest.php new file mode 100644 index 000000000..43a64c27a --- /dev/null +++ b/tests/Unit/V2/WorkflowExecutionProfileTest.php @@ -0,0 +1,83 @@ +collect(static function () use (&$closed): WorkflowExecution { + return WorkflowExecution::startCallback( + static function () use (&$closed): string { + $value = Fiber::suspend('first'); + try { + Fiber::suspend($value); + } catch (RuntimeException $exception) { + return $exception->getMessage(); + } finally { + $closed = true; + } + return 'unthrown'; + }, + ); + }); + $snapshot = $first->snapshot(); + $this->assertTrue($snapshot['balanced']); + $this->assertSame(1, $snapshot['phases']['authoring_start']['calls']); + $this->assertFalse($closed); + $outside = hrtime(true); + usleep(20_000); // Waiting outside the Fiber must not become authoring wall time. + $outside = hrtime(true) - $outside; + $this->assertSame('second', $resumed->collect(static fn () => $execution->send('second'))); + $this->assertNull($resumed->collect(static fn () => $execution->throw(new RuntimeException('caught')))); + $this->assertSame('caught', $execution->getReturn()); + $this->assertTrue($closed); + $this->assertSame($snapshot, $first->snapshot()); + $profile = $resumed->snapshot(); + $this->assertTrue($profile['balanced']); + $this->assertSame(['authoring_resume', 'authoring_throw'], array_keys($profile['phases'])); + $this->assertGreaterThanOrEqual(20_000_000, $outside); + WorkflowExecution::startCallback(static fn (): string => 'disabled'); + $this->assertSame($profile, $resumed->snapshot()); + } + + public function testNestedCpuAndWallBucketsReconcileWhenTheFiberThrows(): void + { + $profile = new WorkflowExecutionProfile(); + $error = new RuntimeException('body failure'); + try { + $profile->collect(static function () use ($error): void { + $sample = WorkflowExecutionProfile::begin('outer'); + try { + WorkflowExecution::startCallback(static function () use ($error): never { + throw $error; + }); + } finally { + WorkflowExecutionProfile::finish($sample); + } + }); + $this->fail('Expected the same body failure.'); + } catch (RuntimeException $caught) { + $this->assertSame($error, $caught); + } + $snapshot = $profile->snapshot(); + $this->assertTrue($snapshot['balanced']); + $outer = $snapshot['phases']['outer']; + $body = $snapshot['phases']['authoring_start']; + $this->assertSame($outer['cpu_us'], $outer['exclusive_cpu_us'] + $body['cpu_us']); + $this->assertSame($outer['wall_ns'], $outer['exclusive_wall_ns'] + $body['wall_ns']); + $this->assertSame($body['cpu_us'], $body['exclusive_cpu_us']); + $this->assertSame($body['wall_ns'], $body['exclusive_wall_ns']); + } +}