From 07c0f82d67870930c5492e207f0698d765680423 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 13 Jun 2026 13:10:38 +0300 Subject: [PATCH 1/5] feat: Nexus samples with end-to-end Feature suite Five samples covering every Nexus handler form: - sync #[Operation] - async #[AsyncOperation] returning WorkflowHandle (SDK-managed start+cancel) - manual #[AsyncOperation] returning an OperationHandlerInterface object (custom token, runtime sync/async choice, own cancel) plus cancellation, context-propagation and multiple-arguments variants. Feature test harness: endpoint-per-test via the gRPC OperatorService, one task queue per scenario, real handlers alongside mocked service/client doubles. --- app/composer.json | 14 +- app/composer.lock | 893 ++++++++++-------- app/src/Nexus/.rr.caller.yaml | 16 + app/src/Nexus/.rr.handler.yaml | 16 + app/src/Nexus/Caller/CallerWorker.php | 11 + app/src/Nexus/Caller/EchoCallerWorkflow.php | 15 + .../Nexus/Caller/EchoCallerWorkflowImpl.php | 35 + app/src/Nexus/Caller/HelloCallerWorkflow.php | 16 + .../Nexus/Caller/HelloCallerWorkflowImpl.php | 36 + .../Caller/HelloWithTokenCallerWorkflow.php | 16 + .../HelloWithTokenCallerWorkflowImpl.php | 47 + app/src/Nexus/ExecuteCommand.php | 84 ++ app/src/Nexus/Handler/EchoClient.php | 13 + app/src/Nexus/Handler/EchoClientImpl.php | 16 + app/src/Nexus/Handler/HandlerWorker.php | 10 + .../Nexus/Handler/HelloHandlerWorkflow.php | 16 + .../Handler/HelloHandlerWorkflowImpl.php | 34 + .../Nexus/Handler/SampleNexusServiceImpl.php | 37 + app/src/Nexus/README.md | 91 ++ app/src/Nexus/Service/EchoInput.php | 12 + app/src/Nexus/Service/EchoOutput.php | 12 + app/src/Nexus/Service/HelloInput.php | 13 + app/src/Nexus/Service/HelloOutput.php | 12 + app/src/Nexus/Service/Language.php | 14 + app/src/Nexus/Service/SampleNexusService.php | 20 + app/src/Nexus/Service/description.md | 5 + app/src/Nexus/caller-worker.php | 30 + app/src/Nexus/handler-worker.php | 37 + app/src/NexusCancellation/.rr.caller.yaml | 16 + app/src/NexusCancellation/.rr.handler.yaml | 16 + .../NexusCancellation/Caller/CallerWorker.php | 11 + .../Caller/HelloCallerWorkflow.php | 15 + .../Caller/HelloCallerWorkflowImpl.php | 63 ++ app/src/NexusCancellation/ExecuteCommand.php | 53 ++ .../Handler/HandlerWorker.php | 10 + .../Handler/HelloHandlerWorkflow.php | 16 + .../Handler/HelloHandlerWorkflowImpl.php | 49 + .../Handler/SampleNexusServiceImpl.php | 23 + app/src/NexusCancellation/README.md | 63 ++ .../NexusCancellation/Service/HelloInput.php | 13 + .../NexusCancellation/Service/HelloOutput.php | 12 + .../NexusCancellation/Service/Language.php | 14 + .../Service/SampleNexusService.php | 16 + app/src/NexusCancellation/caller-worker.php | 24 + app/src/NexusCancellation/handler-worker.php | 37 + .../NexusContextPropagation/.rr.caller.yaml | 16 + .../NexusContextPropagation/.rr.handler.yaml | 16 + .../Caller/CallerWorker.php | 11 + .../Caller/EchoCallerWorkflow.php | 15 + .../Caller/EchoCallerWorkflowImpl.php | 38 + .../Caller/HelloCallerWorkflow.php | 16 + .../Caller/HelloCallerWorkflowImpl.php | 39 + .../ExecuteCommand.php | 69 ++ .../Handler/EchoClient.php | 13 + .../Handler/EchoClientImpl.php | 16 + .../Handler/HandlerWorker.php | 10 + .../Handler/HelloHandlerWorkflow.php | 16 + .../Handler/HelloHandlerWorkflowImpl.php | 34 + .../Handler/SampleNexusServiceImpl.php | 47 + .../Propagation/MDC.php | 32 + .../NexusOutboundContextInterceptor.php | 29 + app/src/NexusContextPropagation/README.md | 77 ++ .../Service/EchoInput.php | 12 + .../Service/EchoOutput.php | 12 + .../Service/HelloInput.php | 13 + .../Service/HelloOutput.php | 12 + .../Service/Language.php | 14 + .../Service/SampleNexusService.php | 20 + .../NexusContextPropagation/caller-worker.php | 34 + .../handler-worker.php | 37 + app/src/NexusManualOperation/.rr.caller.yaml | 16 + app/src/NexusManualOperation/.rr.handler.yaml | 16 + .../Caller/CallerWorker.php | 11 + .../Caller/JobCallerWorkflow.php | 15 + .../Caller/JobCallerWorkflowImpl.php | 73 ++ .../NexusManualOperation/ExecuteCommand.php | 53 ++ .../Handler/ExternalJobClient.php | 18 + .../Handler/HandlerWorker.php | 10 + .../Handler/JobOperationHandler.php | 44 + .../Handler/SampleNexusService.php | 24 + app/src/NexusManualOperation/README.md | 103 ++ .../NexusManualOperation/Service/JobInput.php | 13 + .../Service/JobResult.php | 12 + .../NexusManualOperation/caller-worker.php | 24 + .../NexusManualOperation/handler-worker.php | 24 + .../NexusMultipleArguments/.rr.caller.yaml | 16 + .../NexusMultipleArguments/.rr.handler.yaml | 16 + .../Caller/CallerWorker.php | 11 + .../Caller/EchoCallerWorkflow.php | 15 + .../Caller/EchoCallerWorkflowImpl.php | 35 + .../Caller/HelloCallerWorkflow.php | 16 + .../Caller/HelloCallerWorkflowImpl.php | 36 + .../NexusMultipleArguments/ExecuteCommand.php | 69 ++ .../Handler/EchoClient.php | 13 + .../Handler/EchoClientImpl.php | 16 + .../Handler/HandlerWorker.php | 10 + .../Handler/HelloHandlerWorkflow.php | 16 + .../Handler/HelloHandlerWorkflowImpl.php | 33 + .../Handler/SampleNexusServiceImpl.php | 38 + app/src/NexusMultipleArguments/README.md | 121 +++ .../Service/EchoInput.php | 12 + .../Service/EchoOutput.php | 12 + .../Service/HelloInput.php | 13 + .../Service/HelloOutput.php | 12 + .../Service/Language.php | 14 + .../Service/SampleNexusService.php | 20 + .../NexusMultipleArguments/caller-worker.php | 28 + .../NexusMultipleArguments/handler-worker.php | 37 + app/tests/Feature/.rr.test.yaml | 1 - .../Feature/Nexus/CallerWorkflowMockTest.php | 39 + .../Feature/Nexus/CallerWorkflowTest.php | 47 + app/tests/Feature/Nexus/CancellationTest.php | 31 + .../Feature/Nexus/ContextPropagationTest.php | 32 + .../Feature/Nexus/HelloWithTokenTest.php | 27 + .../Feature/Nexus/ManualOperationTest.php | 28 + .../Nexus/Mock/HeaderEchoNexusServiceImpl.php | 43 + .../Feature/Nexus/Mock/MockEchoClient.php | 24 + .../Mock/MockHelloHandlerWorkflowImpl.php | 25 + .../Nexus/Mock/MockSampleNexusServiceImpl.php | 39 + .../Feature/Nexus/MultipleArgumentsTest.php | 26 + .../Feature/Nexus/NexusEndpointHelper.php | 86 ++ .../Feature/Nexus/NexusServiceMockTest.php | 38 + app/tests/Feature/Nexus/NexusTestCase.php | 59 ++ .../TestCancellationCallerWorkflow.php | 15 + .../TestCancellationCallerWorkflowImpl.php | 61 ++ .../TestContextEchoCallerWorkflow.php | 15 + .../TestContextEchoCallerWorkflowImpl.php | 33 + .../Nexus/Workflow/TestEchoCallerWorkflow.php | 20 + .../Workflow/TestEchoCallerWorkflowImpl.php | 29 + .../Workflow/TestHelloCallerWorkflow.php | 16 + .../Workflow/TestHelloCallerWorkflowImpl.php | 30 + .../TestHelloWithTokenCallerWorkflow.php | 19 + .../TestHelloWithTokenCallerWorkflowImpl.php | 41 + .../Workflow/TestManualJobCallerWorkflow.php | 15 + .../TestManualJobCallerWorkflowImpl.php | 72 ++ .../TestMultiArgsHelloCallerWorkflow.php | 16 + .../TestMultiArgsHelloCallerWorkflowImpl.php | 31 + app/tests/Feature/TestCase.php | 2 +- app/tests/Feature/bootstrap.php | 17 +- app/tests/Feature/worker.php | 124 ++- app/tests/bootstrap.php | 2 +- 141 files changed, 4451 insertions(+), 387 deletions(-) create mode 100644 app/src/Nexus/.rr.caller.yaml create mode 100644 app/src/Nexus/.rr.handler.yaml create mode 100644 app/src/Nexus/Caller/CallerWorker.php create mode 100644 app/src/Nexus/Caller/EchoCallerWorkflow.php create mode 100644 app/src/Nexus/Caller/EchoCallerWorkflowImpl.php create mode 100644 app/src/Nexus/Caller/HelloCallerWorkflow.php create mode 100644 app/src/Nexus/Caller/HelloCallerWorkflowImpl.php create mode 100644 app/src/Nexus/Caller/HelloWithTokenCallerWorkflow.php create mode 100644 app/src/Nexus/Caller/HelloWithTokenCallerWorkflowImpl.php create mode 100644 app/src/Nexus/ExecuteCommand.php create mode 100644 app/src/Nexus/Handler/EchoClient.php create mode 100644 app/src/Nexus/Handler/EchoClientImpl.php create mode 100644 app/src/Nexus/Handler/HandlerWorker.php create mode 100644 app/src/Nexus/Handler/HelloHandlerWorkflow.php create mode 100644 app/src/Nexus/Handler/HelloHandlerWorkflowImpl.php create mode 100644 app/src/Nexus/Handler/SampleNexusServiceImpl.php create mode 100644 app/src/Nexus/README.md create mode 100644 app/src/Nexus/Service/EchoInput.php create mode 100644 app/src/Nexus/Service/EchoOutput.php create mode 100644 app/src/Nexus/Service/HelloInput.php create mode 100644 app/src/Nexus/Service/HelloOutput.php create mode 100644 app/src/Nexus/Service/Language.php create mode 100644 app/src/Nexus/Service/SampleNexusService.php create mode 100644 app/src/Nexus/Service/description.md create mode 100644 app/src/Nexus/caller-worker.php create mode 100644 app/src/Nexus/handler-worker.php create mode 100644 app/src/NexusCancellation/.rr.caller.yaml create mode 100644 app/src/NexusCancellation/.rr.handler.yaml create mode 100644 app/src/NexusCancellation/Caller/CallerWorker.php create mode 100644 app/src/NexusCancellation/Caller/HelloCallerWorkflow.php create mode 100644 app/src/NexusCancellation/Caller/HelloCallerWorkflowImpl.php create mode 100644 app/src/NexusCancellation/ExecuteCommand.php create mode 100644 app/src/NexusCancellation/Handler/HandlerWorker.php create mode 100644 app/src/NexusCancellation/Handler/HelloHandlerWorkflow.php create mode 100644 app/src/NexusCancellation/Handler/HelloHandlerWorkflowImpl.php create mode 100644 app/src/NexusCancellation/Handler/SampleNexusServiceImpl.php create mode 100644 app/src/NexusCancellation/README.md create mode 100644 app/src/NexusCancellation/Service/HelloInput.php create mode 100644 app/src/NexusCancellation/Service/HelloOutput.php create mode 100644 app/src/NexusCancellation/Service/Language.php create mode 100644 app/src/NexusCancellation/Service/SampleNexusService.php create mode 100644 app/src/NexusCancellation/caller-worker.php create mode 100644 app/src/NexusCancellation/handler-worker.php create mode 100644 app/src/NexusContextPropagation/.rr.caller.yaml create mode 100644 app/src/NexusContextPropagation/.rr.handler.yaml create mode 100644 app/src/NexusContextPropagation/Caller/CallerWorker.php create mode 100644 app/src/NexusContextPropagation/Caller/EchoCallerWorkflow.php create mode 100644 app/src/NexusContextPropagation/Caller/EchoCallerWorkflowImpl.php create mode 100644 app/src/NexusContextPropagation/Caller/HelloCallerWorkflow.php create mode 100644 app/src/NexusContextPropagation/Caller/HelloCallerWorkflowImpl.php create mode 100644 app/src/NexusContextPropagation/ExecuteCommand.php create mode 100644 app/src/NexusContextPropagation/Handler/EchoClient.php create mode 100644 app/src/NexusContextPropagation/Handler/EchoClientImpl.php create mode 100644 app/src/NexusContextPropagation/Handler/HandlerWorker.php create mode 100644 app/src/NexusContextPropagation/Handler/HelloHandlerWorkflow.php create mode 100644 app/src/NexusContextPropagation/Handler/HelloHandlerWorkflowImpl.php create mode 100644 app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php create mode 100644 app/src/NexusContextPropagation/Propagation/MDC.php create mode 100644 app/src/NexusContextPropagation/Propagation/NexusOutboundContextInterceptor.php create mode 100644 app/src/NexusContextPropagation/README.md create mode 100644 app/src/NexusContextPropagation/Service/EchoInput.php create mode 100644 app/src/NexusContextPropagation/Service/EchoOutput.php create mode 100644 app/src/NexusContextPropagation/Service/HelloInput.php create mode 100644 app/src/NexusContextPropagation/Service/HelloOutput.php create mode 100644 app/src/NexusContextPropagation/Service/Language.php create mode 100644 app/src/NexusContextPropagation/Service/SampleNexusService.php create mode 100644 app/src/NexusContextPropagation/caller-worker.php create mode 100644 app/src/NexusContextPropagation/handler-worker.php create mode 100644 app/src/NexusManualOperation/.rr.caller.yaml create mode 100644 app/src/NexusManualOperation/.rr.handler.yaml create mode 100644 app/src/NexusManualOperation/Caller/CallerWorker.php create mode 100644 app/src/NexusManualOperation/Caller/JobCallerWorkflow.php create mode 100644 app/src/NexusManualOperation/Caller/JobCallerWorkflowImpl.php create mode 100644 app/src/NexusManualOperation/ExecuteCommand.php create mode 100644 app/src/NexusManualOperation/Handler/ExternalJobClient.php create mode 100644 app/src/NexusManualOperation/Handler/HandlerWorker.php create mode 100644 app/src/NexusManualOperation/Handler/JobOperationHandler.php create mode 100644 app/src/NexusManualOperation/Handler/SampleNexusService.php create mode 100644 app/src/NexusManualOperation/README.md create mode 100644 app/src/NexusManualOperation/Service/JobInput.php create mode 100644 app/src/NexusManualOperation/Service/JobResult.php create mode 100644 app/src/NexusManualOperation/caller-worker.php create mode 100644 app/src/NexusManualOperation/handler-worker.php create mode 100644 app/src/NexusMultipleArguments/.rr.caller.yaml create mode 100644 app/src/NexusMultipleArguments/.rr.handler.yaml create mode 100644 app/src/NexusMultipleArguments/Caller/CallerWorker.php create mode 100644 app/src/NexusMultipleArguments/Caller/EchoCallerWorkflow.php create mode 100644 app/src/NexusMultipleArguments/Caller/EchoCallerWorkflowImpl.php create mode 100644 app/src/NexusMultipleArguments/Caller/HelloCallerWorkflow.php create mode 100644 app/src/NexusMultipleArguments/Caller/HelloCallerWorkflowImpl.php create mode 100644 app/src/NexusMultipleArguments/ExecuteCommand.php create mode 100644 app/src/NexusMultipleArguments/Handler/EchoClient.php create mode 100644 app/src/NexusMultipleArguments/Handler/EchoClientImpl.php create mode 100644 app/src/NexusMultipleArguments/Handler/HandlerWorker.php create mode 100644 app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflow.php create mode 100644 app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflowImpl.php create mode 100644 app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php create mode 100644 app/src/NexusMultipleArguments/README.md create mode 100644 app/src/NexusMultipleArguments/Service/EchoInput.php create mode 100644 app/src/NexusMultipleArguments/Service/EchoOutput.php create mode 100644 app/src/NexusMultipleArguments/Service/HelloInput.php create mode 100644 app/src/NexusMultipleArguments/Service/HelloOutput.php create mode 100644 app/src/NexusMultipleArguments/Service/Language.php create mode 100644 app/src/NexusMultipleArguments/Service/SampleNexusService.php create mode 100644 app/src/NexusMultipleArguments/caller-worker.php create mode 100644 app/src/NexusMultipleArguments/handler-worker.php create mode 100644 app/tests/Feature/Nexus/CallerWorkflowMockTest.php create mode 100644 app/tests/Feature/Nexus/CallerWorkflowTest.php create mode 100644 app/tests/Feature/Nexus/CancellationTest.php create mode 100644 app/tests/Feature/Nexus/ContextPropagationTest.php create mode 100644 app/tests/Feature/Nexus/HelloWithTokenTest.php create mode 100644 app/tests/Feature/Nexus/ManualOperationTest.php create mode 100644 app/tests/Feature/Nexus/Mock/HeaderEchoNexusServiceImpl.php create mode 100644 app/tests/Feature/Nexus/Mock/MockEchoClient.php create mode 100644 app/tests/Feature/Nexus/Mock/MockHelloHandlerWorkflowImpl.php create mode 100644 app/tests/Feature/Nexus/Mock/MockSampleNexusServiceImpl.php create mode 100644 app/tests/Feature/Nexus/MultipleArgumentsTest.php create mode 100644 app/tests/Feature/Nexus/NexusEndpointHelper.php create mode 100644 app/tests/Feature/Nexus/NexusServiceMockTest.php create mode 100644 app/tests/Feature/Nexus/NexusTestCase.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflow.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflowImpl.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflow.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflowImpl.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflow.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflowImpl.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflow.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflowImpl.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflow.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflowImpl.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflow.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflowImpl.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php create mode 100644 app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflowImpl.php diff --git a/app/composer.json b/app/composer.json index 5e5344d..0aa9700 100644 --- a/app/composer.json +++ b/app/composer.json @@ -1,9 +1,15 @@ { "minimum-stability": "dev", "prefer-stable": true, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/temporalio/sdk-php" + } + ], "require": { "spiral/tokenizer": "^3.16.0", - "temporal/sdk": "^2.16", + "temporal/sdk": "dev-nexus-new as 2.18.0", "temporal/open-telemetry-interceptors": "^1.0", "open-telemetry/exporter-otlp": "^1.3", "open-telemetry/transport-grpc": "^1.1", @@ -12,7 +18,8 @@ "require-dev": { "buggregator/trap": "^1.15", "internal/dload": "^1.8.0", - "phpunit/phpunit": "^10.5" + "phpunit/phpunit": "^10.5", + "phpstan/phpstan": "^2.2" }, "autoload": { "psr-4": { @@ -33,6 +40,7 @@ }, "scripts": { "get:binaries": "dload get --no-interaction -vv", - "test:feat": "phpunit --testsuite=Feature --color=always --testdox" + "test:feat": "phpunit --testsuite=Feature --color=always --testdox", + "phpstan": "phpstan analyse --memory-limit=2G" } } diff --git a/app/composer.lock b/app/composer.lock index b4950a3..9faac84 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "87733b8fcc9eb5e9b37f5e271e8b10c3", + "content-hash": "bb34ba5229d49452bfefff2f47439cc5", "packages": [ { "name": "brick/math", - "version": "0.14.1", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", - "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.1" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2025-11-24T14:40:29+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -214,20 +214,20 @@ }, { "name": "google/common-protos", - "version": "4.12.4", + "version": "4.14.0", "source": { "type": "git", "url": "https://github.com/googleapis/common-protos-php.git", - "reference": "0127156899af0df2681bd42024c60bd5360d64e3" + "reference": "f8e72f7b581702e7c3ee0776144f4974da172428" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/common-protos-php/zipball/0127156899af0df2681bd42024c60bd5360d64e3", - "reference": "0127156899af0df2681bd42024c60bd5360d64e3", + "url": "https://api.github.com/repos/googleapis/common-protos-php/zipball/f8e72f7b581702e7c3ee0776144f4974da172428", + "reference": "f8e72f7b581702e7c3ee0776144f4974da172428", "shasum": "" }, "require": { - "google/protobuf": "^4.31", + "google/protobuf": "^4.31||^5.0", "php": "^8.1" }, "require-dev": { @@ -267,29 +267,29 @@ "google" ], "support": { - "source": "https://github.com/googleapis/common-protos-php/tree/v4.12.4" + "source": "https://github.com/googleapis/common-protos-php/tree/v4.14.0" }, - "time": "2025-09-20T01:29:44+00:00" + "time": "2026-04-09T21:01:46+00:00" }, { "name": "google/protobuf", - "version": "v4.33.2", + "version": "v4.33.6", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318" + "reference": "84b008c23915ed94536737eae46f41ba3bccfe67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", - "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/84b008c23915ed94536737eae46f41ba3bccfe67", + "reference": "84b008c23915ed94536737eae46f41ba3bccfe67", "shasum": "" }, "require": { "php": ">=8.1.0" }, "require-dev": { - "phpunit/phpunit": ">=5.0.0 <8.5.27" + "phpunit/phpunit": ">=10.5.62 <11.0.0" }, "suggest": { "ext-bcmath": "Need to support JSON deserialization" @@ -311,26 +311,26 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.2" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.6" }, - "time": "2025-12-05T22:12:22+00:00" + "time": "2026-03-18T17:32:05+00:00" }, { "name": "grpc/grpc", - "version": "1.74.0", + "version": "1.81.0", "source": { "type": "git", "url": "https://github.com/grpc/grpc-php.git", - "reference": "32bf4dba256d60d395582fb6e4e8d3936bcdb713" + "reference": "47046d6b2a6cc7e68806a287d6a1fee57e0c40da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/grpc/grpc-php/zipball/32bf4dba256d60d395582fb6e4e8d3936bcdb713", - "reference": "32bf4dba256d60d395582fb6e4e8d3936bcdb713", + "url": "https://api.github.com/repos/grpc/grpc-php/zipball/47046d6b2a6cc7e68806a287d6a1fee57e0c40da", + "reference": "47046d6b2a6cc7e68806a287d6a1fee57e0c40da", "shasum": "" }, "require": { - "php": ">=7.0.0" + "php": ">=7.1.0" }, "require-dev": { "google/auth": "^v1.3.0" @@ -355,9 +355,9 @@ "rpc" ], "support": { - "source": "https://github.com/grpc/grpc-php/tree/v1.74.0" + "source": "https://github.com/grpc/grpc-php/tree/v1.81.0" }, - "time": "2025-07-24T20:02:16+00:00" + "time": "2026-05-20T09:30:50+00:00" }, { "name": "internal/destroy", @@ -493,16 +493,16 @@ }, { "name": "nesbot/carbon", - "version": "3.11.0", + "version": "3.11.4", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "bdb375400dcd162624531666db4799b36b64e4a1" + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", - "reference": "bdb375400dcd162624531666db4799b36b64e4a1", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", "shasum": "" }, "require": { @@ -526,7 +526,7 @@ "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.22", "phpunit/phpunit": "^10.5.53", - "squizlabs/php_codesniffer": "^3.13.4" + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, "bin": [ "bin/carbon" @@ -569,14 +569,14 @@ } ], "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", + "homepage": "https://carbonphp.github.io/carbon/", "keywords": [ "date", "datetime", "time" ], "support": { - "docs": "https://carbon.nesbot.com/docs", + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", "issues": "https://github.com/CarbonPHP/carbon/issues", "source": "https://github.com/CarbonPHP/carbon" }, @@ -594,7 +594,7 @@ "type": "tidelift" } ], - "time": "2025-12-02T21:04:28+00:00" + "time": "2026-04-07T09:57:54+00:00" }, { "name": "nyholm/psr7-server", @@ -664,16 +664,16 @@ }, { "name": "open-telemetry/api", - "version": "1.7.1", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4" + "reference": "6f8d237ce2c304ca85f31970f788e7f074d147be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4", - "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/6f8d237ce2c304ca85f31970f788e7f074d147be", + "reference": "6f8d237ce2c304ca85f31970f788e7f074d147be", "shasum": "" }, "require": { @@ -683,7 +683,7 @@ "symfony/polyfill-php82": "^1.26" }, "conflict": { - "open-telemetry/sdk": "<=1.0.8" + "open-telemetry/sdk": "<=1.11" }, "type": "library", "extra": { @@ -730,20 +730,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-10-19T10:49:48+00:00" + "time": "2026-02-25T13:24:05+00:00" }, { "name": "open-telemetry/context", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/context.git", - "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf" + "reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf", - "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/3c414b246e0dabb7d6145404e6a5e4536ca18d07", + "reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07", "shasum": "" }, "require": { @@ -785,24 +785,24 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-19T00:05:49+00:00" + "time": "2025-10-19T06:44:33+00:00" }, { "name": "open-telemetry/exporter-otlp", - "version": "1.3.3", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d" + "reference": "283a0d66522f2adc6d8d7debfd7686be91c282be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/07b02bc71838463f6edcc78d3485c04b48fb263d", - "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/283a0d66522f2adc6d8d7debfd7686be91c282be", + "reference": "283a0d66522f2adc6d8d7debfd7686be91c282be", "shasum": "" }, "require": { @@ -853,20 +853,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-11-13T08:04:37+00:00" + "time": "2026-02-05T09:44:52+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", - "version": "1.8.0", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", - "reference": "673af5b06545b513466081884b47ef15a536edde" + "reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde", - "reference": "673af5b06545b513466081884b47ef15a536edde", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/a229cf161d42001d64c8f21e8f678581fe1c66b9", + "reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9", "shasum": "" }, "require": { @@ -912,30 +912,30 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-17T23:10:12+00:00" + "time": "2025-10-19T06:44:33+00:00" }, { "name": "open-telemetry/sdk", - "version": "1.10.0", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99" + "reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", - "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/6e3d0ce93e76555dd5e2f1d19443ff45b990e410", + "reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "^1.7", + "open-telemetry/api": "^1.8", "open-telemetry/context": "^1.4", "open-telemetry/sem-conv": "^1.0", "php": "^8.1", @@ -970,7 +970,7 @@ ] }, "branch-alias": { - "dev-main": "1.9.x-dev" + "dev-main": "1.12.x-dev" } }, "autoload": { @@ -1013,20 +1013,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-11-25T10:59:15+00:00" + "time": "2026-03-21T11:50:01+00:00" }, { "name": "open-telemetry/sem-conv", - "version": "1.37.0", + "version": "1.38.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sem-conv.git", - "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1" + "reference": "e613bc640a407def4991b8a936a9b27edd9a3240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/8da7ec497c881e39afa6657d72586e27efbd29a1", - "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/e613bc640a407def4991b8a936a9b27edd9a3240", + "reference": "e613bc640a407def4991b8a936a9b27edd9a3240", "shasum": "" }, "require": { @@ -1066,24 +1066,24 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-03T12:08:10+00:00" + "time": "2026-01-21T04:14:03+00:00" }, { "name": "open-telemetry/transport-grpc", - "version": "1.1.3", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/transport-grpc.git", - "reference": "763cc620552d871fe2050fa58644398827651576" + "reference": "c276f1dbb43ca735b8850b19809100b7acd3fd60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/transport-grpc/zipball/763cc620552d871fe2050fa58644398827651576", - "reference": "763cc620552d871fe2050fa58644398827651576", + "url": "https://api.github.com/repos/opentelemetry-php/transport-grpc/zipball/c276f1dbb43ca735b8850b19809100b7acd3fd60", + "reference": "c276f1dbb43ca735b8850b19809100b7acd3fd60", "shasum": "" }, "require": { @@ -1128,11 +1128,11 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-03-04T22:35:49+00:00" + "time": "2025-10-19T06:44:33+00:00" }, { "name": "php-http/discovery", @@ -1830,20 +1830,20 @@ }, { "name": "roadrunner-php/roadrunner-api-dto", - "version": "v1.14.0", + "version": "v1.14.1", "source": { "type": "git", "url": "https://github.com/roadrunner-php/roadrunner-api-dto.git", - "reference": "e6efb759f0a73b8516b7f28317230ecd4010005e" + "reference": "a7664d4a4c451631fba7d5504fdf4f94a81a19c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/roadrunner-php/roadrunner-api-dto/zipball/e6efb759f0a73b8516b7f28317230ecd4010005e", - "reference": "e6efb759f0a73b8516b7f28317230ecd4010005e", + "url": "https://api.github.com/repos/roadrunner-php/roadrunner-api-dto/zipball/a7664d4a4c451631fba7d5504fdf4f94a81a19c3", + "reference": "a7664d4a4c451631fba7d5504fdf4f94a81a19c3", "shasum": "" }, "require": { - "google/protobuf": "^4.31.1", + "google/protobuf": "^4.31.1||^5.34.0", "php": "^8.1" }, "conflict": { @@ -1885,7 +1885,7 @@ "docs": "https://docs.roadrunner.dev", "forum": "https://forum.roadrunner.dev", "issues": "https://github.com/roadrunner-server/roadrunner/issues", - "source": "https://github.com/roadrunner-php/roadrunner-api-dto/tree/v1.14.0" + "source": "https://github.com/roadrunner-php/roadrunner-api-dto/tree/v1.14.1" }, "funding": [ { @@ -1893,27 +1893,27 @@ "type": "github" } ], - "time": "2025-11-06T13:03:11+00:00" + "time": "2026-02-28T14:55:02+00:00" }, { "name": "roadrunner-php/version-checker", - "version": "v1.0.2", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/roadrunner-php/version-checker.git", - "reference": "a7994f700586265a54a2989b97f7d7f25ed5890b" + "reference": "ab3bf69887754160cc9df05d086e281f4a9fe084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/roadrunner-php/version-checker/zipball/a7994f700586265a54a2989b97f7d7f25ed5890b", - "reference": "a7994f700586265a54a2989b97f7d7f25ed5890b", + "url": "https://api.github.com/repos/roadrunner-php/version-checker/zipball/ab3bf69887754160cc9df05d086e281f4a9fe084", + "reference": "ab3bf69887754160cc9df05d086e281f4a9fe084", "shasum": "" }, "require": { "composer-runtime-api": "^2.0", "composer/semver": "^3.3", "php": "^8.0", - "symfony/process": "^5.4 || ^6.0 || ^7.0" + "symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.8", @@ -1938,7 +1938,7 @@ "version-checker" ], "support": { - "source": "https://github.com/roadrunner-php/version-checker/tree/v1.0.2" + "source": "https://github.com/roadrunner-php/version-checker/tree/v1.1.0" }, "funding": [ { @@ -1946,7 +1946,7 @@ "type": "github" } ], - "time": "2025-05-20T08:45:05+00:00" + "time": "2026-02-17T16:54:55+00:00" }, { "name": "spiral/attributes", @@ -2032,22 +2032,22 @@ }, { "name": "spiral/core", - "version": "3.16.0", + "version": "3.17.0", "source": { "type": "git", "url": "https://github.com/spiral/core.git", - "reference": "700db0337f7ba4d8ce87d102bc51c5820182edec" + "reference": "faa40cb998ac0ba58b24cb599315026fe276f099" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spiral/core/zipball/700db0337f7ba4d8ce87d102bc51c5820182edec", - "reference": "700db0337f7ba4d8ce87d102bc51c5820182edec", + "url": "https://api.github.com/repos/spiral/core/zipball/faa40cb998ac0ba58b24cb599315026fe276f099", + "reference": "faa40cb998ac0ba58b24cb599315026fe276f099", "shasum": "" }, "require": { "php": ">=8.1", "psr/container": "^1.1|^2.0", - "spiral/security": "^3.16" + "spiral/security": "^3.17" }, "provide": { "psr/container-implementation": "^1.1|^2.0" @@ -2060,7 +2060,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.16.x-dev" + "dev-master": "3.17.x-dev" } }, "autoload": { @@ -2102,20 +2102,20 @@ "type": "github" } ], - "time": "2025-12-14T11:35:42+00:00" + "time": "2026-06-03T10:04:47+00:00" }, { "name": "spiral/goridge", - "version": "4.2.1", + "version": "4.2.2", "source": { "type": "git", "url": "https://github.com/roadrunner-php/goridge.git", - "reference": "2a372118dac1f0c0511e2862f963ce649fefd9fa" + "reference": "a14454ce6c9cfcec76bbd283292c25fb33fc5a43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/roadrunner-php/goridge/zipball/2a372118dac1f0c0511e2862f963ce649fefd9fa", - "reference": "2a372118dac1f0c0511e2862f963ce649fefd9fa", + "url": "https://api.github.com/repos/roadrunner-php/goridge/zipball/a14454ce6c9cfcec76bbd283292c25fb33fc5a43", + "reference": "a14454ce6c9cfcec76bbd283292c25fb33fc5a43", "shasum": "" }, "require": { @@ -2181,7 +2181,7 @@ "chat": "https://discord.gg/V6EK4he", "docs": "https://docs.roadrunner.dev", "issues": "https://github.com/roadrunner-server/roadrunner/issues", - "source": "https://github.com/roadrunner-php/goridge/tree/4.2.1" + "source": "https://github.com/roadrunner-php/goridge/tree/4.2.2" }, "funding": [ { @@ -2189,27 +2189,27 @@ "type": "github" } ], - "time": "2025-05-05T13:55:33+00:00" + "time": "2026-05-11T11:29:46+00:00" }, { "name": "spiral/hmvc", - "version": "3.16.0", + "version": "3.17.0", "source": { "type": "git", "url": "https://github.com/spiral/hmvc.git", - "reference": "cff7c60a72fd8a590f5d7e13a07244b39627aa5d" + "reference": "42ff28d261ef5335d80b33423f642aeb20f5420e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spiral/hmvc/zipball/cff7c60a72fd8a590f5d7e13a07244b39627aa5d", - "reference": "cff7c60a72fd8a590f5d7e13a07244b39627aa5d", + "url": "https://api.github.com/repos/spiral/hmvc/zipball/42ff28d261ef5335d80b33423f642aeb20f5420e", + "reference": "42ff28d261ef5335d80b33423f642aeb20f5420e", "shasum": "" }, "require": { "php": ">=8.1", "psr/event-dispatcher": "^1.0", - "spiral/core": "^3.16", - "spiral/interceptors": "^3.16" + "spiral/core": "^3.17", + "spiral/interceptors": "^3.17" }, "require-dev": { "phpunit/phpunit": "^10.5.41", @@ -2219,7 +2219,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.16.x-dev" + "dev-master": "3.17.x-dev" } }, "autoload": { @@ -2263,26 +2263,26 @@ "type": "github" } ], - "time": "2025-12-14T11:38:02+00:00" + "time": "2026-06-03T10:08:18+00:00" }, { "name": "spiral/interceptors", - "version": "3.16.0", + "version": "3.17.0", "source": { "type": "git", "url": "https://github.com/spiral/interceptors.git", - "reference": "715f274b35d10565f1c910eb573162179e33b0e7" + "reference": "b5b5712311b51a06c64f2197ee5ac69d1a1c56f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spiral/interceptors/zipball/715f274b35d10565f1c910eb573162179e33b0e7", - "reference": "715f274b35d10565f1c910eb573162179e33b0e7", + "url": "https://api.github.com/repos/spiral/interceptors/zipball/b5b5712311b51a06c64f2197ee5ac69d1a1c56f0", + "reference": "b5b5712311b51a06c64f2197ee5ac69d1a1c56f0", "shasum": "" }, "require": { "php": ">=8.1", "psr/event-dispatcher": "^1.0", - "spiral/core": "^3.16" + "spiral/core": "^3.17" }, "require-dev": { "phpunit/phpunit": "^10.5.41", @@ -2292,7 +2292,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.16.x-dev" + "dev-master": "3.17.x-dev" } }, "autoload": { @@ -2341,26 +2341,26 @@ "type": "github" } ], - "time": "2025-12-14T11:34:19+00:00" + "time": "2026-06-03T10:09:12+00:00" }, { "name": "spiral/logger", - "version": "3.16.0", + "version": "3.17.0", "source": { "type": "git", "url": "https://github.com/spiral/logger.git", - "reference": "bd902bbcdbb6f247a51a75fc716dba3831966f06" + "reference": "bbb7a2f5dc27f6e7c06de48e904d5f7d2aadc9a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spiral/logger/zipball/bd902bbcdbb6f247a51a75fc716dba3831966f06", - "reference": "bd902bbcdbb6f247a51a75fc716dba3831966f06", + "url": "https://api.github.com/repos/spiral/logger/zipball/bbb7a2f5dc27f6e7c06de48e904d5f7d2aadc9a8", + "reference": "bbb7a2f5dc27f6e7c06de48e904d5f7d2aadc9a8", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "1 - 3", - "spiral/core": "^3.16" + "spiral/core": "^3.17" }, "require-dev": { "mockery/mockery": "^1.6.12", @@ -2370,7 +2370,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.16.x-dev" + "dev-master": "3.17.x-dev" } }, "autoload": { @@ -2412,20 +2412,20 @@ "type": "github" } ], - "time": "2025-12-14T11:36:52+00:00" + "time": "2026-06-03T10:06:03+00:00" }, { "name": "spiral/roadrunner", - "version": "v2025.1.6", + "version": "v2025.1.14", "source": { "type": "git", "url": "https://github.com/roadrunner-server/roadrunner.git", - "reference": "207b2b4ba75f2529ecccf801b3d8dd7038f22732" + "reference": "3853ad693522e82d53d62950e5f1315402c910f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/roadrunner-server/roadrunner/zipball/207b2b4ba75f2529ecccf801b3d8dd7038f22732", - "reference": "207b2b4ba75f2529ecccf801b3d8dd7038f22732", + "url": "https://api.github.com/repos/roadrunner-server/roadrunner/zipball/3853ad693522e82d53d62950e5f1315402c910f2", + "reference": "3853ad693522e82d53d62950e5f1315402c910f2", "shasum": "" }, "type": "metapackage", @@ -2453,7 +2453,7 @@ "chat": "https://discord.gg/V6EK4he", "docs": "https://docs.roadrunner.dev/", "issues": "https://github.com/roadrunner-server/roadrunner/issues", - "source": "https://github.com/roadrunner-server/roadrunner/tree/v2025.1.6" + "source": "https://github.com/roadrunner-server/roadrunner/tree/v2025.1.14" }, "funding": [ { @@ -2461,7 +2461,7 @@ "type": "github" } ], - "time": "2025-12-11T14:45:22+00:00" + "time": "2026-05-15T13:20:01+00:00" }, { "name": "spiral/roadrunner-cli", @@ -2703,33 +2703,33 @@ }, { "name": "spiral/security", - "version": "3.16.0", + "version": "3.17.0", "source": { "type": "git", "url": "https://github.com/spiral/security.git", - "reference": "e47affcd2c1718209cae1fe0c3cf54c522db7b1a" + "reference": "52bab8373ba0172256321cb7cdc9baf06567eb66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spiral/security/zipball/e47affcd2c1718209cae1fe0c3cf54c522db7b1a", - "reference": "e47affcd2c1718209cae1fe0c3cf54c522db7b1a", + "url": "https://api.github.com/repos/spiral/security/zipball/52bab8373ba0172256321cb7cdc9baf06567eb66", + "reference": "52bab8373ba0172256321cb7cdc9baf06567eb66", "shasum": "" }, "require": { "php": ">=8.1", - "spiral/core": "^3.16", - "spiral/hmvc": "^3.16" + "spiral/core": "^3.17", + "spiral/hmvc": "^3.17" }, "require-dev": { "mockery/mockery": "^1.6.12", "phpunit/phpunit": "^10.5.41", - "spiral/console": "^3.16", + "spiral/console": "^3.17", "vimeo/psalm": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.16.x-dev" + "dev-master": "3.17.x-dev" } }, "autoload": { @@ -2771,41 +2771,41 @@ "type": "github" } ], - "time": "2025-12-14T11:39:30+00:00" + "time": "2026-06-03T10:05:17+00:00" }, { "name": "spiral/tokenizer", - "version": "3.16.0", + "version": "3.17.0", "source": { "type": "git", "url": "https://github.com/spiral/tokenizer.git", - "reference": "11611160800f959ff0ea329470b939a2c0898d1e" + "reference": "118c5b352cc343d81a1daa117dcd2fd80692523f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spiral/tokenizer/zipball/11611160800f959ff0ea329470b939a2c0898d1e", - "reference": "11611160800f959ff0ea329470b939a2c0898d1e", + "url": "https://api.github.com/repos/spiral/tokenizer/zipball/118c5b352cc343d81a1daa117dcd2fd80692523f", + "reference": "118c5b352cc343d81a1daa117dcd2fd80692523f", "shasum": "" }, "require": { "ext-tokenizer": "*", "php": ">=8.1", - "spiral/core": "^3.16", - "spiral/logger": "^3.16", + "spiral/core": "^3.17", + "spiral/logger": "^3.17", "symfony/finder": "^6.4.30 || ^7.4 || ^8.0" }, "require-dev": { "mockery/mockery": "^1.6.12", "phpunit/phpunit": "^10.5.41", "spiral/attributes": "^2.8|^3.0", - "spiral/boot": "^3.16", - "spiral/files": "^3.16", + "spiral/boot": "^3.17", + "spiral/files": "^3.17", "vimeo/psalm": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.16.x-dev" + "dev-master": "3.17.x-dev" } }, "autoload": { @@ -2847,24 +2847,24 @@ "type": "github" } ], - "time": "2025-12-14T11:39:35+00:00" + "time": "2026-06-03T10:08:42+00:00" }, { "name": "symfony/clock", - "version": "v8.0.0", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", - "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "psr/clock": "^1.0" }, "provide": { @@ -2904,7 +2904,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v8.0.0" + "source": "https://github.com/symfony/clock/tree/v8.1.0" }, "funding": [ { @@ -2924,20 +2924,20 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:46:48+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/console", - "version": "v7.4.1", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", - "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { @@ -3002,7 +3002,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.1" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -3022,20 +3022,20 @@ "type": "tidelift" } ], - "time": "2025-12-05T15:23:39+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -3048,7 +3048,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -3073,7 +3073,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -3084,34 +3084,39 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.0", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/99aec13b82b4967ec5088222c4a3ecca955949c2", + "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0|^8.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3139,7 +3144,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + "source": "https://github.com/symfony/filesystem/tree/v8.1.0" }, "funding": [ { @@ -3159,24 +3164,24 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/finder", - "version": "v8.0.0", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291" + "reference": "58d2e767a66052c1487356f953445634a8194c64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291", - "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291", + "url": "https://api.github.com/repos/symfony/finder/zipball/58d2e767a66052c1487356f953445634a8194c64", + "reference": "58d2e767a66052c1487356f953445634a8194c64", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.4.1" }, "require-dev": { "symfony/filesystem": "^7.4|^8.0" @@ -3207,7 +3212,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.0" + "source": "https://github.com/symfony/finder/tree/v8.1.0" }, "funding": [ { @@ -3227,35 +3232,32 @@ "type": "tidelift" } ], - "time": "2025-11-05T14:36:47+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/http-client", - "version": "v7.4.1", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "26cc224ea7103dda90e9694d9e139a389092d007" + "reference": "68a48e4c31f63fcd1bdff997a85a09e55efe8cdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/26cc224ea7103dda90e9694d9e139a389092d007", - "reference": "26cc224ea7103dda90e9694d9e139a389092d007", + "url": "https://api.github.com/repos/symfony/http-client/zipball/68a48e4c31f63fcd1bdff997a85a09e55efe8cdb", + "reference": "68a48e4c31f63fcd1bdff997a85a09e55efe8cdb", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4.1", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "~3.4.4|^3.5.2", - "symfony/polyfill-php83": "^1.29", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/http-client-contracts": "^3.7", "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "amphp/amp": "<2.5", - "amphp/socket": "<1.1", - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.4" + "amphp/amp": "<3", + "php-http/discovery": "<1.15" }, "provide": { "php-http/async-client-implementation": "*", @@ -3264,20 +3266,19 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/http-client": "^4.2.1|^5.0", - "amphp/http-tunnel": "^1.0|^2.0", - "guzzlehttp/promises": "^1.4|^2.0", + "amphp/http-client": "^5.3.2", + "amphp/http-tunnel": "^2.0", + "guzzlehttp/guzzle": "^7.10", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0" + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3308,7 +3309,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.1" + "source": "https://github.com/symfony/http-client/tree/v8.1.0" }, "funding": [ { @@ -3328,20 +3329,20 @@ "type": "tidelift" } ], - "time": "2025-12-04T21:12:57+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "75d7043853a42837e68111812f4d964b01e5101c" + "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", - "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", + "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", "shasum": "" }, "require": { @@ -3354,7 +3355,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -3390,7 +3391,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.0" }, "funding": [ { @@ -3401,25 +3402,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-29T11:18:49+00:00" + "time": "2026-03-06T13:17:50+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -3469,7 +3474,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -3489,20 +3494,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -3551,7 +3556,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -3571,20 +3576,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -3636,7 +3641,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -3656,20 +3661,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -3721,7 +3726,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -3741,20 +3746,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php82", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php82.git", - "reference": "5d2ed36f7734637dacc025f179698031951b1692" + "reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", - "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/34808efe3e68f69685796f7c253a2f1d8ea9df59", + "reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59", "shasum": "" }, "require": { @@ -3801,7 +3806,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php82/tree/v1.37.0" }, "funding": [ { @@ -3821,20 +3826,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.33.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", "shasum": "" }, "require": { @@ -3881,7 +3886,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" }, "funding": [ { @@ -3901,24 +3906,24 @@ "type": "tidelift" } ], - "time": "2025-07-08T02:45:35+00:00" + "time": "2026-05-27T06:51:48+00:00" }, { "name": "symfony/process", - "version": "v7.4.0", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "url": "https://api.github.com/repos/symfony/process/zipball/c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4.1" }, "type": "library", "autoload": { @@ -3946,7 +3951,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.0" + "source": "https://github.com/symfony/process/tree/v8.1.0" }, "funding": [ { @@ -3966,20 +3971,20 @@ "type": "tidelift" } ], - "time": "2025-10-16T11:21:06+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -3997,7 +4002,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -4033,7 +4038,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -4053,24 +4058,24 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v8.0.1", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", - "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-intl-grapheme": "^1.33", "symfony/polyfill-intl-normalizer": "^1.0", @@ -4123,7 +4128,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.1" + "source": "https://github.com/symfony/string/tree/v8.1.0" }, "funding": [ { @@ -4143,24 +4148,24 @@ "type": "tidelift" } ], - "time": "2025-12-01T09:13:36+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/translation", - "version": "v8.0.1", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "770e3b8b0ba8360958abedcabacd4203467333ca" + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/770e3b8b0ba8360958abedcabacd4203467333ca", - "reference": "770e3b8b0ba8360958abedcabacd4203467333ca", + "url": "https://api.github.com/repos/symfony/translation/zipball/b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-mbstring": "^1.0", "symfony/translation-contracts": "^3.6.1" }, @@ -4216,7 +4221,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.0.1" + "source": "https://github.com/symfony/translation/tree/v8.1.0" }, "funding": [ { @@ -4236,20 +4241,20 @@ "type": "tidelift" } ], - "time": "2025-12-01T09:13:36+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", - "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", "shasum": "" }, "require": { @@ -4262,7 +4267,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -4298,7 +4303,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" }, "funding": [ { @@ -4318,31 +4323,32 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/yaml", - "version": "v8.0.1", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14" + "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14", - "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14", + "url": "https://api.github.com/repos/symfony/yaml/zipball/efb42bd2c6f4f3ccfd4683583449938b5fc146b0", + "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<7.4" }, "require-dev": { - "symfony/console": "^7.4|^8.0" + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" }, "bin": [ "Resources/bin/yaml-lint" @@ -4373,7 +4379,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.0.1" + "source": "https://github.com/symfony/yaml/tree/v8.1.0" }, "funding": [ { @@ -4393,7 +4399,7 @@ "type": "tidelift" } ], - "time": "2025-12-04T18:17:06+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "tbachert/spi", @@ -4449,23 +4455,23 @@ }, { "name": "temporal/open-telemetry-interceptors", - "version": "v1.0.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/temporalio/sdk-php-interceptors-opentelemetry.git", - "reference": "6f9e0021865ba4c7ba764673f57f152b4291c6c4" + "reference": "6885dfb09a048264b84f59bcc384fd90000f08d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/temporalio/sdk-php-interceptors-opentelemetry/zipball/6f9e0021865ba4c7ba764673f57f152b4291c6c4", - "reference": "6f9e0021865ba4c7ba764673f57f152b4291c6c4", + "url": "https://api.github.com/repos/temporalio/sdk-php-interceptors-opentelemetry/zipball/6885dfb09a048264b84f59bcc384fd90000f08d2", + "reference": "6885dfb09a048264b84f59bcc384fd90000f08d2", "shasum": "" }, "require": { "open-telemetry/sdk": "^1.4", "php": ">=8.1", "symfony/polyfill-php83": "^1.32", - "temporal/sdk": "^2.14.1" + "temporal/sdk": "^2.17" }, "require-dev": { "open-telemetry/api": "^1.3.0", @@ -4504,57 +4510,61 @@ "support": { "chat": "https://t.mp/slack", "docs": "https://docs.temporal.io/", - "source": "https://github.com/temporalio/sdk-php-interceptors-opentelemetry/tree/v1.0.0" + "source": "https://github.com/temporalio/sdk-php-interceptors-opentelemetry/tree/v1.1.1" }, - "time": "2025-09-25T08:43:53+00:00" + "time": "2026-03-16T09:18:16+00:00" }, { "name": "temporal/sdk", - "version": "v2.16.0", + "version": "dev-nexus-new", "source": { "type": "git", "url": "https://github.com/temporalio/sdk-php.git", - "reference": "dfa5fb0a2f2e674c3f1731dd7ad16dd99deb0fef" + "reference": "923f673be9d0ba0c4c3d75b0a2c2442dd8ae42fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/dfa5fb0a2f2e674c3f1731dd7ad16dd99deb0fef", - "reference": "dfa5fb0a2f2e674c3f1731dd7ad16dd99deb0fef", + "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/923f673be9d0ba0c4c3d75b0a2c2442dd8ae42fb", + "reference": "923f673be9d0ba0c4c3d75b0a2c2442dd8ae42fb", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", "google/common-protos": "^4.9", - "google/protobuf": "^4.31.1", + "google/protobuf": "^4.33.6 || ^5.34.0", "grpc/grpc": "^1.57", "internal/destroy": "^1.0", - "internal/promise": "^2.12 || ^3.4", + "internal/promise": "^3.4", "nesbot/carbon": "^2.72.6 || ^3.8.4", "php": ">=8.1", + "psr/clock": "^1.0", "psr/log": "^2.0 || ^3.0.2", "ramsey/uuid": "^4.7.6", - "roadrunner-php/roadrunner-api-dto": "^1.13.0", - "roadrunner-php/version-checker": "^1.0.1", + "roadrunner-php/roadrunner-api-dto": "^1.14.0", + "roadrunner-php/version-checker": "^1.0.2", "spiral/attributes": "^3.1.8", - "spiral/roadrunner": "^2025.1.3", + "spiral/roadrunner": "^2025.1.5", "spiral/roadrunner-cli": "^2.6", "spiral/roadrunner-kv": "^4.3.1", "spiral/roadrunner-worker": "^3.6.2", - "symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0", - "symfony/http-client": "^5.4.49 || ^6.4.17 || ^7.0", + "symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0 || ^8.0", + "symfony/http-client": "^5.4.53 || ^6.4.17 || ^7.0 || ^8.0", "symfony/polyfill-php83": "^1.31.0", - "symfony/process": "^5.4.47 || ^6.4.15 || ^7.0" + "symfony/process": "^5.4.51 || ^6.4.15 || ^7.0 || ^8.0" }, "require-dev": { "buggregator/trap": "^1.13.0", "composer/composer": "^2.8.4", + "cweagans/composer-patches": "^2.0", "dereuromark/composer-prefer-lowest": "^0.1.10", "doctrine/annotations": "^1.14.4 || ^2.0.2", - "internal/dload": "^1.2.0", + "ext-simplexml": "*", + "internal/dload": "^1.10.0", + "internal/toml": "^1.0.3", "jetbrains/phpstorm-attributes": "dev-master", "laminas/laminas-code": "^4.16", - "phpunit/phpunit": "10.5.45", + "phpunit/phpunit": "10.5.63", "spiral/code-style": "~2.3.0", "spiral/core": "^3.14.9", "ta-tikoma/phpunit-architecture-test": "^0.8.5", @@ -4564,21 +4574,88 @@ "buggregator/trap": "For better debugging", "ext-grpc": "For Client calls", "ext-protobuf": "For better performance", + "internal/toml": "To load TOML config files", "roadrunner/psr-logger": "RoadRunner PSR-3 logger integration" }, "type": "library", + "extra": { + "patches": { + "phpunit/phpunit": { + "Improve assertInstanceOf error message": "patches/phpunit-instance-of.patch", + "Make runTest protected": "patches/phpunit-run-test-protected.patch" + } + } + }, "autoload": { + "psr-4": { + "Temporal\\Api\\Testservice\\": "testing/api/testservice/Temporal/Api/Testservice", + "GPBMetadata\\Temporal\\Api\\Testservice\\": "testing/api/testservice/GPBMetadata/Temporal/Api/Testservice", + "Temporal\\Testing\\": "testing/src", + "Temporal\\": "src" + }, "files": [ "src/include.php" - ], + ] + }, + "autoload-dev": { "psr-4": { - "Temporal\\": "src", - "Temporal\\Testing\\": "testing/src", - "Temporal\\Api\\Testservice\\": "testing/api/testservice/Temporal/Api/Testservice", - "GPBMetadata\\Temporal\\Api\\Testservice\\": "testing/api/testservice/GPBMetadata/Temporal/Api/Testservice" - } + "Temporal\\Tests\\Acceptance\\App\\": "tests/Acceptance/App", + "Temporal\\Tests\\Interceptor\\": "tests/Fixtures/src/Interceptor", + "Temporal\\Tests\\Workflow\\": "tests/Fixtures/src/Workflow", + "Temporal\\Tests\\Activity\\": "tests/Fixtures/src/Activity", + "Temporal\\Tests\\DTO\\": "tests/Fixtures/src/DTO", + "Temporal\\Tests\\Proto\\": "tests/Fixtures/src/Proto", + "Temporal\\Tests\\": "tests" + } + }, + "scripts": { + "get:binaries": [ + "dload get --no-interaction -vv", + "Temporal\\Worker\\Transport\\RoadRunnerVersionChecker::postUpdate" + ], + "cs:diff": [ + "php-cs-fixer fix --dry-run -v --diff --show-progress dots" + ], + "cs:fix": [ + "php-cs-fixer fix -v" + ], + "psalm": [ + "psalm" + ], + "psalm:baseline": [ + "psalm --set-baseline=psalm-baseline.xml" + ], + "test:unit": [ + "tests/runner.php vendor/bin/phpunit --testsuite=Unit --color=always --testdox" + ], + "test:func": [ + "tests/runner.php vendor/bin/phpunit --testsuite=Functional --color=always --testdox" + ], + "test:arch": [ + "phpunit --testsuite=Arch --color=always --testdox" + ], + "test:accept": [ + "tests/runner.php vendor/bin/phpunit --testsuite=Acceptance --color=always --testdox" + ], + "test:accept-slow": [ + "tests/runner.php vendor/bin/phpunit --testsuite=\"Acceptance-Slow\" --color=always --testdox" + ], + "test:accept-fast": [ + "tests/runner.php vendor/bin/phpunit --testsuite=\"Acceptance-Fast\" --color=always --testdox" + ], + "transcripts:last": [ + "php tests/Acceptance/transcript-merge.php" + ], + "transcripts:list": [ + "php tests/Acceptance/transcript-merge.php --list" + ], + "transcripts:merge": [ + "php tests/Acceptance/transcript-merge.php" + ], + "transcripts:clean": [ + "rm -rf runtime/tests/transcripts/*" + ] }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -4595,27 +4672,27 @@ "workflow" ], "support": { - "docs": "https://docs.temporal.io", "forum": "https://community.temporal.io", + "docs": "https://docs.temporal.io", "issues": "https://github.com/temporalio/sdk-php/issues", "source": "https://github.com/temporalio/sdk-php" }, - "time": "2025-10-06T17:30:29+00:00" + "time": "2026-06-10T12:19:19+00:00" } ], "packages-dev": [ { "name": "buggregator/trap", - "version": "1.15.0", + "version": "1.15.2", "source": { "type": "git", "url": "https://github.com/buggregator/trap.git", - "reference": "a4f46cc638144856901710a6ec7569bf3c929052" + "reference": "f146f04ee7be14740d8b35311f1a136a929976ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/buggregator/trap/zipball/a4f46cc638144856901710a6ec7569bf3c929052", - "reference": "a4f46cc638144856901710a6ec7569bf3c929052", + "url": "https://api.github.com/repos/buggregator/trap/zipball/f146f04ee7be14740d8b35311f1a136a929976ed", + "reference": "f146f04ee7be14740d8b35311f1a136a929976ed", "shasum": "" }, "require": { @@ -4625,6 +4702,7 @@ "internal/destroy": "^1.0", "nyholm/psr7": "^1.8", "php": ">=8.1", + "php-64bit": "*", "php-http/message": "^1.15", "psr/container": "^1.1 || ^2.0", "psr/http-message": "^1.1 || ^2", @@ -4692,7 +4770,7 @@ ], "support": { "issues": "https://github.com/buggregator/trap/issues", - "source": "https://github.com/buggregator/trap/tree/1.15.0" + "source": "https://github.com/buggregator/trap/tree/1.15.2" }, "funding": [ { @@ -4700,7 +4778,7 @@ "type": "boosty" } ], - "time": "2025-11-30T13:26:02+00:00" + "time": "2026-04-07T21:19:09+00:00" }, { "name": "clue/stream-filter", @@ -4770,16 +4848,16 @@ }, { "name": "internal/dload", - "version": "1.8.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/php-internal/dload.git", - "reference": "be366a424843f266b22354d06d5243fe898eb1a2" + "reference": "f23d3b8ec0d22996777f839e8275b1d7b5fbbbda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-internal/dload/zipball/be366a424843f266b22354d06d5243fe898eb1a2", - "reference": "be366a424843f266b22354d06d5243fe898eb1a2", + "url": "https://api.github.com/repos/php-internal/dload/zipball/f23d3b8ec0d22996777f839e8275b1d7b5fbbbda", + "reference": "f23d3b8ec0d22996777f839e8275b1d7b5fbbbda", "shasum": "" }, "require": { @@ -4794,13 +4872,14 @@ "react/async": "^3.2 || ^4.3", "react/promise": "^2.10 || ^3.2", "symfony/console": "^6.4 || ^7 || ^8", - "symfony/http-client": "^4.4 || ^5.4 || ^6.4 || ^7 || ^8", + "symfony/http-client": "^6.4 || ^7 || ^8", "yiisoft/injector": "^1.2" }, "require-dev": { "buggregator/trap": "^1.15", "dereuromark/composer-prefer-lowest": "^0.1.10", "phpunit/phpunit": "^10.5", + "roxblnfk/unpoly": "^1.8.1", "spiral/code-style": "^2.2.2", "ta-tikoma/phpunit-architecture-test": "^0.8.5", "vimeo/psalm": "^6.10" @@ -4834,7 +4913,7 @@ ], "support": { "issues": "https://github.com/php-internal/dload/issues", - "source": "https://github.com/php-internal/dload/tree/1.8.0" + "source": "https://github.com/php-internal/dload/tree/1.12.0" }, "funding": [ { @@ -4842,7 +4921,7 @@ "type": "boosty" } ], - "time": "2025-12-17T06:29:45+00:00" + "time": "2026-04-28T14:06:17+00:00" }, { "name": "internal/path", @@ -4909,16 +4988,16 @@ }, { "name": "internal/toml", - "version": "1.0.3", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-internal/toml.git", - "reference": "519d4d1c523249250a2e01a8c63d7f41e5be5d70" + "reference": "c6b2769e7b8ff25a9667337767eff6ec7967405c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-internal/toml/zipball/519d4d1c523249250a2e01a8c63d7f41e5be5d70", - "reference": "519d4d1c523249250a2e01a8c63d7f41e5be5d70", + "url": "https://api.github.com/repos/php-internal/toml/zipball/c6b2769e7b8ff25a9667337767eff6ec7967405c", + "reference": "c6b2769e7b8ff25a9667337767eff6ec7967405c", "shasum": "" }, "require": { @@ -4926,6 +5005,7 @@ }, "require-dev": { "buggregator/trap": "^1.13", + "internal/dload": "^1.11", "phpunit/phpunit": "^10.5", "spiral/code-style": "^2.3.0", "ta-tikoma/phpunit-architecture-test": "^0.8.5", @@ -4947,13 +5027,13 @@ "homepage": "https://github.com/roxblnfk" } ], - "description": "TOML support for PHP", + "description": "TOML 1.0/1.1 parser and encoder", "keywords": [ "toml" ], "support": { "issues": "https://github.com/php-internal/toml/issues", - "source": "https://github.com/php-internal/toml/tree/1.0.3" + "source": "https://github.com/php-internal/toml/tree/1.1.2" }, "funding": [ { @@ -4961,7 +5041,7 @@ "type": "patreon" } ], - "time": "2025-11-17T15:05:00+00:00" + "time": "2026-03-31T11:16:07+00:00" }, { "name": "myclabs/deep-copy", @@ -5346,6 +5426,70 @@ }, "time": "2024-10-02T11:34:13+00:00" }, + { + "name": "phpstan/phpstan", + "version": "2.2.1", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660", + "reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660", + "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-05-28T14:44:12+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "10.1.16", @@ -5669,16 +5813,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.60", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f2e26f52f80ef77832e359205f216eeac00e320c" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f2e26f52f80ef77832e359205f216eeac00e320c", - "reference": "f2e26f52f80ef77832e359205f216eeac00e320c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -5699,7 +5843,7 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.4", + "sebastian/comparator": "^5.0.5", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.4", @@ -5750,7 +5894,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.60" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -5774,7 +5918,7 @@ "type": "tidelift" } ], - "time": "2025-12-06T07:50:42+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "react/async", @@ -6093,16 +6237,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -6158,7 +6302,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { @@ -6178,7 +6322,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T05:25:07+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", @@ -6878,16 +7022,16 @@ }, { "name": "symfony/var-dumper", - "version": "v8.0.0", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "d2a2476c93b58ac5292145e9fac1ff76a21d1ce2" + "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/d2a2476c93b58ac5292145e9fac1ff76a21d1ce2", - "reference": "d2a2476c93b58ac5292145e9fac1ff76a21d1ce2", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", + "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", "shasum": "" }, "require": { @@ -6941,7 +7085,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.0.0" + "source": "https://github.com/symfony/var-dumper/tree/v8.0.8" }, "funding": [ { @@ -6961,7 +7105,7 @@ "type": "tidelift" } ], - "time": "2025-10-28T09:34:19+00:00" + "time": "2026-03-31T07:15:36+00:00" }, { "name": "theseer/tokenizer", @@ -7090,12 +7234,21 @@ "time": "2025-12-01T11:14:17+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "temporal/sdk", + "version": "dev-nexus-new", + "alias": "2.18.0", + "alias_normalized": "2.18.0.0" + } + ], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "temporal/sdk": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": {}, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/app/src/Nexus/.rr.caller.yaml b/app/src/Nexus/.rr.caller.yaml new file mode 100644 index 0000000..3d46805 --- /dev/null +++ b/app/src/Nexus/.rr.caller.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6102 + +server: + command: "php caller-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-caller-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/Nexus/.rr.handler.yaml b/app/src/Nexus/.rr.handler.yaml new file mode 100644 index 0000000..36ff019 --- /dev/null +++ b/app/src/Nexus/.rr.handler.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6101 + +server: + command: "php handler-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-target-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/Nexus/Caller/CallerWorker.php b/app/src/Nexus/Caller/CallerWorker.php new file mode 100644 index 0000000..7b3076b --- /dev/null +++ b/app/src/Nexus/Caller/CallerWorker.php @@ -0,0 +1,11 @@ +sampleNexusService = Workflow::newNexusServiceStub( + SampleNexusService::class, + NexusOperationOptions::new() + ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)), + ); + } + + public function echo(string $message) + { + /** @var EchoOutput $output */ + $output = yield $this->sampleNexusService->echo(new EchoInput($message)); + return $output->message; + } +} diff --git a/app/src/Nexus/Caller/HelloCallerWorkflow.php b/app/src/Nexus/Caller/HelloCallerWorkflow.php new file mode 100644 index 0000000..0ff02e2 --- /dev/null +++ b/app/src/Nexus/Caller/HelloCallerWorkflow.php @@ -0,0 +1,16 @@ +sampleNexusService = Workflow::newNexusServiceStub( + SampleNexusService::class, + NexusOperationOptions::new() + ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)), + ); + } + + public function hello(string $message, Language $language) + { + /** @var HelloOutput $output */ + $output = yield $this->sampleNexusService->hello(new HelloInput($message, $language)); + return $output->message; + } +} diff --git a/app/src/Nexus/Caller/HelloWithTokenCallerWorkflow.php b/app/src/Nexus/Caller/HelloWithTokenCallerWorkflow.php new file mode 100644 index 0000000..e20bb80 --- /dev/null +++ b/app/src/Nexus/Caller/HelloWithTokenCallerWorkflow.php @@ -0,0 +1,16 @@ +stub = Workflow::newUntypedNexusOperationStub( + NexusOperationOptions::new() + ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withService('SampleNexusService') + ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)), + ); + } + + public function hello(string $message, Language $language) + { + /** @var NexusOperationHandle $handle */ + $handle = yield $this->stub->start( + 'hello', + [new HelloInput($message, $language)], + HelloOutput::class, + ); + + $token = $handle->getOperationToken() ?? ''; + Workflow::getLogger()->info("Nexus operation started, token: {$token}"); + + /** @var HelloOutput $output */ + $output = yield $handle->getResult(); + + return "[token={$token}] {$output->message}"; + } +} diff --git a/app/src/Nexus/ExecuteCommand.php b/app/src/Nexus/ExecuteCommand.php new file mode 100644 index 0000000..a543f8c --- /dev/null +++ b/app/src/Nexus/ExecuteCommand.php @@ -0,0 +1,84 @@ +withNamespace('my-caller-namespace'), + ); + + $output->writeln("Starting EchoCallerWorkflow..."); + $echoWorkflow = $client->newWorkflowStub( + EchoCallerWorkflow::class, + WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), + ); + $run = $client->start($echoWorkflow, 'Nexus Echo πŸ‘‹'); + $execution = $run->getExecution(); + $output->writeln(\sprintf( + 'Started: WorkflowID=%s RunID=%s', + $execution->getID(), + $execution->getRunID(), + )); + $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); + + $output->writeln("\nStarting HelloCallerWorkflow..."); + $helloWorkflow = $client->newWorkflowStub( + HelloCallerWorkflow::class, + WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), + ); + $run = $client->start($helloWorkflow, 'Nexus', Language::ES); + $execution = $run->getExecution(); + $output->writeln(\sprintf( + 'Started: WorkflowID=%s RunID=%s', + $execution->getID(), + $execution->getRunID(), + )); + $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); + + $output->writeln("\nStarting HelloWithTokenCallerWorkflow..."); + $tokenWorkflow = $client->newWorkflowStub( + HelloWithTokenCallerWorkflow::class, + WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), + ); + $run = $client->start($tokenWorkflow, 'Nexus', Language::FR); + $execution = $run->getExecution(); + $output->writeln(\sprintf( + 'Started: WorkflowID=%s RunID=%s', + $execution->getID(), + $execution->getRunID(), + )); + $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); + + return self::SUCCESS; + } +} diff --git a/app/src/Nexus/Handler/EchoClient.php b/app/src/Nexus/Handler/EchoClient.php new file mode 100644 index 0000000..a40f63b --- /dev/null +++ b/app/src/Nexus/Handler/EchoClient.php @@ -0,0 +1,13 @@ +message); + } +} diff --git a/app/src/Nexus/Handler/HandlerWorker.php b/app/src/Nexus/Handler/HandlerWorker.php new file mode 100644 index 0000000..a76182c --- /dev/null +++ b/app/src/Nexus/Handler/HandlerWorker.php @@ -0,0 +1,10 @@ +language) { + case Language::EN: + return new HelloOutput("Hello {$input->name} πŸ‘‹"); + case Language::FR: + return new HelloOutput("Bonjour {$input->name} πŸ‘‹"); + case Language::DE: + return new HelloOutput("Hallo {$input->name} πŸ‘‹"); + case Language::ES: + return new HelloOutput("Β‘Hola! {$input->name} πŸ‘‹"); + case Language::TR: + return new HelloOutput("Merhaba {$input->name} πŸ‘‹"); + } + throw new ApplicationFailure( + "Unsupported language: {$input->language->value}", + 'UNSUPPORTED_LANGUAGE', + false, + ); + } +} diff --git a/app/src/Nexus/Handler/SampleNexusServiceImpl.php b/app/src/Nexus/Handler/SampleNexusServiceImpl.php new file mode 100644 index 0000000..07e839c --- /dev/null +++ b/app/src/Nexus/Handler/SampleNexusServiceImpl.php @@ -0,0 +1,37 @@ +echoClient = $echoClient ?? new EchoClientImpl(); + } + + public function echo(EchoInput $input): EchoOutput + { + return $this->echoClient->echo($input); + } + + public function hello(HelloInput $input): WorkflowHandle + { + return WorkflowHandle::fromWorkflowMethod( + HelloHandlerWorkflow::class, + WorkflowOptions::new()->withWorkflowId(Nexus::getStartDetails()->requestId), + $input, + ); + } +} diff --git a/app/src/Nexus/README.md b/app/src/Nexus/README.md new file mode 100644 index 0000000..ae6d4e1 --- /dev/null +++ b/app/src/Nexus/README.md @@ -0,0 +1,91 @@ +# Nexus sample + +A caller workflow in `my-caller-namespace` invokes operations on a Nexus +service hosted by a handler worker in `my-target-namespace`. Two operations: + +- `echo` β€” synchronous (`#[Operation]`) +- `hello` β€” async, `WorkflowRunOperation`, starts `HelloHandlerWorkflow` + server-side (`#[AsyncOperation(output: HelloOutput::class)]`) + +Three caller workflows: + +- `EchoCallerWorkflow` β€” typed stub, sync op. +- `HelloCallerWorkflow` β€” typed stub, async op (the typed proxy resolves + directly to `HelloOutput`, the operation token stays hidden). +- `HelloWithTokenCallerWorkflow` β€” same call routed through the **untyped** + stub via `Workflow::newUntypedNexusOperationStub()`. `start()` returns a + `NexusOperationHandle` that exposes `operationToken` (string for async, + null for sync) before `getResult()` resolves with the typed result. Use + this when you need the token β€” for cancel-by-token, linking, logging. + +## Prerequisites + +Beyond the usual (`./temporal`, `./rr`): + +```bash +./temporal operator namespace create --namespace my-target-namespace +./temporal operator namespace create --namespace my-caller-namespace + +./temporal operator nexus endpoint create \ + --name my-nexus-endpoint-name \ + --target-namespace my-target-namespace \ + --target-task-queue my-handler-task-queue \ + --description-file ./app/src/Nexus/Service/description.md +``` + +## Run + +Three terminals. + +**Handler worker** (`my-target-namespace`): + +```bash +cd app/src/Nexus +TEMPORAL_NAMESPACE=my-target-namespace ../../rr serve -c .rr.handler.yaml +``` + +**Caller worker** (`my-caller-namespace`): + +```bash +cd app/src/Nexus +TEMPORAL_NAMESPACE=my-caller-namespace ../../rr serve -c .rr.caller.yaml +``` + +**Starter**: + +```bash +php app/app.php nexus +``` + +Expected output: + +``` +Starting EchoCallerWorkflow... +Started: WorkflowID=... RunID=... +Result: Nexus Echo πŸ‘‹ + +Starting HelloCallerWorkflow... +Started: WorkflowID=... RunID=... +Result: Β‘Hola! Nexus πŸ‘‹ + +Starting HelloWithTokenCallerWorkflow... +Started: WorkflowID=... RunID=... +Result: [token=ChJteS10YXJnZXQt...] Bonjour Nexus πŸ‘‹ +``` + +The `token=...` prefix in the third line is the server-issued operation +token, base64-ish blob encoding `namespace + workflowId`. It is only +present for async operations. + +## Constants + +| Name | Value | +|---|---| +| service | `SampleNexusService` | +| operations | `echo`, `hello` | +| endpoint | `my-nexus-endpoint-name` | +| handler task queue | `my-handler-task-queue` | +| caller task queue | `my-caller-workflow-task-queue` | +| target namespace | `my-target-namespace` | +| caller namespace | `my-caller-namespace` | +| `scheduleToCloseTimeout` | `10s` | diff --git a/app/src/Nexus/Service/EchoInput.php b/app/src/Nexus/Service/EchoInput.php new file mode 100644 index 0000000..eacb207 --- /dev/null +++ b/app/src/Nexus/Service/EchoInput.php @@ -0,0 +1,12 @@ +newWorker(CallerWorker::TASK_QUEUE) + ->registerWorkflowTypes( + EchoCallerWorkflowImpl::class, + HelloCallerWorkflowImpl::class, + HelloWithTokenCallerWorkflowImpl::class, + ); + +$factory->run(); diff --git a/app/src/Nexus/handler-worker.php b/app/src/Nexus/handler-worker.php new file mode 100644 index 0000000..f2c1ad6 --- /dev/null +++ b/app/src/Nexus/handler-worker.php @@ -0,0 +1,37 @@ +withNamespace($namespace), +); + +$factory = WorkerFactory::create(client: $workflowClient); + +$worker = $factory->newWorker(HandlerWorker::TASK_QUEUE); +$worker->registerWorkflowTypes(HelloHandlerWorkflowImpl::class); +$worker->registerNexusServiceImplementation(new SampleNexusServiceImpl()); + +$factory->run(); diff --git a/app/src/NexusCancellation/.rr.caller.yaml b/app/src/NexusCancellation/.rr.caller.yaml new file mode 100644 index 0000000..3d46805 --- /dev/null +++ b/app/src/NexusCancellation/.rr.caller.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6102 + +server: + command: "php caller-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-caller-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/NexusCancellation/.rr.handler.yaml b/app/src/NexusCancellation/.rr.handler.yaml new file mode 100644 index 0000000..36ff019 --- /dev/null +++ b/app/src/NexusCancellation/.rr.handler.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6101 + +server: + command: "php handler-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-target-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/NexusCancellation/Caller/CallerWorker.php b/app/src/NexusCancellation/Caller/CallerWorker.php new file mode 100644 index 0000000..73ec213 --- /dev/null +++ b/app/src/NexusCancellation/Caller/CallerWorker.php @@ -0,0 +1,11 @@ +sampleNexusService = Workflow::newNexusServiceStub( + SampleNexusService::class, + NexusOperationOptions::new() + ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)) + ->withCancellationType(NexusOperationCancellationType::WaitRequested), + ); + } + + public function hello(string $message) + { + $promises = []; + + $scope = Workflow::async(function () use ($message, &$promises): void { + foreach (Language::cases() as $language) { + $promises[] = $this->sampleNexusService->hello(new HelloInput($message, $language)); + } + }); + + /** @var HelloOutput $first */ + $first = yield Promise::any($promises); + + $scope->cancel(); + + foreach ($promises as $promise) { + try { + yield $promise; + } catch (CanceledFailure) { + } catch (NexusOperationFailure $e) { + if (!$e->getPrevious() instanceof CanceledFailure) { + throw $e; + } + } + } + + return $first->message; + } +} diff --git a/app/src/NexusCancellation/ExecuteCommand.php b/app/src/NexusCancellation/ExecuteCommand.php new file mode 100644 index 0000000..67fc57c --- /dev/null +++ b/app/src/NexusCancellation/ExecuteCommand.php @@ -0,0 +1,53 @@ +withNamespace('my-caller-namespace'), + ); + + $output->writeln("Starting HelloCallerWorkflow..."); + $workflow = $client->newWorkflowStub( + HelloCallerWorkflow::class, + WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), + ); + $run = $client->start($workflow, 'Nexus'); + $execution = $run->getExecution(); + $output->writeln(\sprintf( + 'Started: WorkflowID=%s RunID=%s', + $execution->getID(), + $execution->getRunID(), + )); + $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); + + return self::SUCCESS; + } +} diff --git a/app/src/NexusCancellation/Handler/HandlerWorker.php b/app/src/NexusCancellation/Handler/HandlerWorker.php new file mode 100644 index 0000000..d43ad2b --- /dev/null +++ b/app/src/NexusCancellation/Handler/HandlerWorker.php @@ -0,0 +1,10 @@ + \random_int(0, 4)))); + + switch ($input->language) { + case Language::EN: + return new HelloOutput("Hello {$input->name} πŸ‘‹"); + case Language::FR: + return new HelloOutput("Bonjour {$input->name} πŸ‘‹"); + case Language::DE: + return new HelloOutput("Hallo {$input->name} πŸ‘‹"); + case Language::ES: + return new HelloOutput("Β‘Hola! {$input->name} πŸ‘‹"); + case Language::TR: + return new HelloOutput("Merhaba {$input->name} πŸ‘‹"); + } + throw new ApplicationFailure( + "Unsupported language: {$input->language->value}", + 'UNSUPPORTED_LANGUAGE', + false, + ); + } catch (CanceledFailure $e) { + yield Workflow::asyncDetached(function () { + yield Workflow::timer(CarbonInterval::seconds(yield Workflow::sideEffect(fn() => \random_int(0, 4)))); + }); + + Workflow::getLogger()->info('HelloHandlerWorkflow was cancelled successfully.'); + + throw $e; + } + } +} diff --git a/app/src/NexusCancellation/Handler/SampleNexusServiceImpl.php b/app/src/NexusCancellation/Handler/SampleNexusServiceImpl.php new file mode 100644 index 0000000..7359fcf --- /dev/null +++ b/app/src/NexusCancellation/Handler/SampleNexusServiceImpl.php @@ -0,0 +1,23 @@ +withWorkflowId(Nexus::getStartDetails()->requestId), + $input, + ); + } +} diff --git a/app/src/NexusCancellation/README.md b/app/src/NexusCancellation/README.md new file mode 100644 index 0000000..8fc9da8 --- /dev/null +++ b/app/src/NexusCancellation/README.md @@ -0,0 +1,63 @@ +# Nexus Cancellation sample + +A caller workflow fans out the `hello` Nexus operation in 5 languages in +parallel. After the first reply arrives it cancels the cancellation scope β€” +the other 4 in-flight operations get cancellation requests, the handler +workflows clean up and rethrow `CanceledFailure`, and the caller drains the +promises (ignoring `CanceledFailure`) before returning the first result. + +Defines its own single-operation `SampleNexusService` (no `echo`), modeled +on the [Nexus sample](../Nexus/README.md). Its `HelloHandlerWorkflowImpl` +(in `Handler/`) adds a random delay and a detached cleanup scope. + +Caller passes `NexusOperationCancellationType::WaitRequested` so it returns +as soon as the handler acknowledges the cancellation request β€” it doesn't +wait for the detached cleanup scope to finish on the handler side. + +## Prerequisites + +Same setup as the [Nexus sample](../Nexus/README.md) β€” namespaces and the +endpoint must already exist. **Stop any regular-Nexus workers first**: both +samples share `my-handler-task-queue` / `my-caller-workflow-task-queue` and +register the same workflow type names. + +## Run + +Three terminals. + +**Handler worker** (`my-target-namespace`): + +```bash +cd app/src/NexusCancellation +TEMPORAL_NAMESPACE=my-target-namespace ../../rr serve -c .rr.handler.yaml +``` + +**Caller worker** (`my-caller-namespace`): + +```bash +cd app/src/NexusCancellation +TEMPORAL_NAMESPACE=my-caller-namespace ../../rr serve -c .rr.caller.yaml +``` + +**Starter**: + +```bash +php app/app.php nexus-cancellation +``` + +Caller side prints the first reply (whichever language won the race): + +``` +Starting HelloCallerWorkflow... +Started: WorkflowID=... RunID=... +Result: Β‘Hola! Nexus πŸ‘‹ +``` + +Handler side logs each cancelled run: + +``` +HelloHandlerWorkflow was cancelled successfully. +HelloHandlerWorkflow was cancelled successfully. +HelloHandlerWorkflow was cancelled successfully. +HelloHandlerWorkflow was cancelled successfully. +``` diff --git a/app/src/NexusCancellation/Service/HelloInput.php b/app/src/NexusCancellation/Service/HelloInput.php new file mode 100644 index 0000000..0f86464 --- /dev/null +++ b/app/src/NexusCancellation/Service/HelloInput.php @@ -0,0 +1,13 @@ +newWorker(CallerWorker::TASK_QUEUE) + ->registerWorkflowTypes(HelloCallerWorkflowImpl::class); + +$factory->run(); diff --git a/app/src/NexusCancellation/handler-worker.php b/app/src/NexusCancellation/handler-worker.php new file mode 100644 index 0000000..b5064ff --- /dev/null +++ b/app/src/NexusCancellation/handler-worker.php @@ -0,0 +1,37 @@ +withNamespace($namespace), +); + +$factory = WorkerFactory::create(client: $workflowClient); + +$worker = $factory->newWorker(HandlerWorker::TASK_QUEUE); +$worker->registerWorkflowTypes(HelloHandlerWorkflowImpl::class); +$worker->registerNexusServiceImplementation(new SampleNexusServiceImpl()); + +$factory->run(); diff --git a/app/src/NexusContextPropagation/.rr.caller.yaml b/app/src/NexusContextPropagation/.rr.caller.yaml new file mode 100644 index 0000000..3d46805 --- /dev/null +++ b/app/src/NexusContextPropagation/.rr.caller.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6102 + +server: + command: "php caller-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-caller-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/NexusContextPropagation/.rr.handler.yaml b/app/src/NexusContextPropagation/.rr.handler.yaml new file mode 100644 index 0000000..36ff019 --- /dev/null +++ b/app/src/NexusContextPropagation/.rr.handler.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6101 + +server: + command: "php handler-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-target-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/NexusContextPropagation/Caller/CallerWorker.php b/app/src/NexusContextPropagation/Caller/CallerWorker.php new file mode 100644 index 0000000..0c82b17 --- /dev/null +++ b/app/src/NexusContextPropagation/Caller/CallerWorker.php @@ -0,0 +1,11 @@ +sampleNexusService = Workflow::newNexusServiceStub( + SampleNexusService::class, + NexusOperationOptions::new() + ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)), + ); + } + + public function echo(string $message) + { + MDC::put('x-nexus-caller-workflow-id', Workflow::getInfo()->execution->getID()); + + /** @var EchoOutput $output */ + $output = yield $this->sampleNexusService->echo(new EchoInput($message)); + return $output->message; + } +} diff --git a/app/src/NexusContextPropagation/Caller/HelloCallerWorkflow.php b/app/src/NexusContextPropagation/Caller/HelloCallerWorkflow.php new file mode 100644 index 0000000..81f013d --- /dev/null +++ b/app/src/NexusContextPropagation/Caller/HelloCallerWorkflow.php @@ -0,0 +1,16 @@ +sampleNexusService = Workflow::newNexusServiceStub( + SampleNexusService::class, + NexusOperationOptions::new() + ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)), + ); + } + + public function hello(string $message, Language $language) + { + MDC::put('x-nexus-caller-workflow-id', Workflow::getInfo()->execution->getID()); + + /** @var HelloOutput $output */ + $output = yield $this->sampleNexusService->hello(new HelloInput($message, $language)); + return $output->message; + } +} diff --git a/app/src/NexusContextPropagation/ExecuteCommand.php b/app/src/NexusContextPropagation/ExecuteCommand.php new file mode 100644 index 0000000..fc9a52e --- /dev/null +++ b/app/src/NexusContextPropagation/ExecuteCommand.php @@ -0,0 +1,69 @@ +withNamespace('my-caller-namespace'), + ); + + $output->writeln("Starting EchoCallerWorkflow..."); + $echoWorkflow = $client->newWorkflowStub( + EchoCallerWorkflow::class, + WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), + ); + $run = $client->start($echoWorkflow, 'Nexus Echo πŸ‘‹'); + $execution = $run->getExecution(); + $output->writeln(\sprintf( + 'Started: WorkflowID=%s RunID=%s', + $execution->getID(), + $execution->getRunID(), + )); + $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); + + $output->writeln("\nStarting HelloCallerWorkflow..."); + $helloWorkflow = $client->newWorkflowStub( + HelloCallerWorkflow::class, + WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), + ); + $run = $client->start($helloWorkflow, 'Nexus', Language::EN); + $execution = $run->getExecution(); + $output->writeln(\sprintf( + 'Started: WorkflowID=%s RunID=%s', + $execution->getID(), + $execution->getRunID(), + )); + $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); + + return self::SUCCESS; + } +} diff --git a/app/src/NexusContextPropagation/Handler/EchoClient.php b/app/src/NexusContextPropagation/Handler/EchoClient.php new file mode 100644 index 0000000..8d82ee6 --- /dev/null +++ b/app/src/NexusContextPropagation/Handler/EchoClient.php @@ -0,0 +1,13 @@ +message); + } +} diff --git a/app/src/NexusContextPropagation/Handler/HandlerWorker.php b/app/src/NexusContextPropagation/Handler/HandlerWorker.php new file mode 100644 index 0000000..fd2285d --- /dev/null +++ b/app/src/NexusContextPropagation/Handler/HandlerWorker.php @@ -0,0 +1,10 @@ +language) { + case Language::EN: + return new HelloOutput("Hello {$input->name} πŸ‘‹"); + case Language::FR: + return new HelloOutput("Bonjour {$input->name} πŸ‘‹"); + case Language::DE: + return new HelloOutput("Hallo {$input->name} πŸ‘‹"); + case Language::ES: + return new HelloOutput("Β‘Hola! {$input->name} πŸ‘‹"); + case Language::TR: + return new HelloOutput("Merhaba {$input->name} πŸ‘‹"); + } + throw new ApplicationFailure( + "Unsupported language: {$input->language->value}", + 'UNSUPPORTED_LANGUAGE', + false, + ); + } +} diff --git a/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php b/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php new file mode 100644 index 0000000..79c26df --- /dev/null +++ b/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php @@ -0,0 +1,47 @@ +echoClient = $echoClient ?? new EchoClientImpl(); + } + + public function echo(EchoInput $input): EchoOutput + { + $headers = Nexus::getCurrentOperationContext()->headers; + if (($id = $headers->get('x-nexus-caller-workflow-id')) !== null) { + \error_log("Echo called from a workflow with ID : {$id}"); + } + + return $this->echoClient->echo($input); + } + + public function hello(HelloInput $input): WorkflowHandle + { + $headers = Nexus::getCurrentOperationContext()->headers; + if (($id = $headers->get('x-nexus-caller-workflow-id')) !== null) { + \error_log("HelloHandlerWorkflow called from a workflow with ID : {$id}"); + } + + return WorkflowHandle::fromWorkflowMethod( + HelloHandlerWorkflow::class, + WorkflowOptions::new()->withWorkflowId(Nexus::getStartDetails()->requestId), + $input, + ); + } +} diff --git a/app/src/NexusContextPropagation/Propagation/MDC.php b/app/src/NexusContextPropagation/Propagation/MDC.php new file mode 100644 index 0000000..0174c43 --- /dev/null +++ b/app/src/NexusContextPropagation/Propagation/MDC.php @@ -0,0 +1,32 @@ + */ + private static array $context = []; + + public static function put(string $key, string $value): void + { + self::$context[$key] = $value; + } + + public static function get(string $key): ?string + { + return self::$context[$key] ?? null; + } + + /** @return array */ + public static function getAll(): array + { + return self::$context; + } + + public static function clear(): void + { + self::$context = []; + } +} diff --git a/app/src/NexusContextPropagation/Propagation/NexusOutboundContextInterceptor.php b/app/src/NexusContextPropagation/Propagation/NexusOutboundContextInterceptor.php new file mode 100644 index 0000000..626897f --- /dev/null +++ b/app/src/NexusContextPropagation/Propagation/NexusOutboundContextInterceptor.php @@ -0,0 +1,29 @@ +nexusHeaders; + foreach (MDC::getAll() as $key => $value) { + if (\str_starts_with($key, self::NEXUS_HEADER_PREFIX)) { + $headers[$key] = $value; + } + } + + return $next($input->with(nexusHeaders: $headers)); + } +} diff --git a/app/src/NexusContextPropagation/README.md b/app/src/NexusContextPropagation/README.md new file mode 100644 index 0000000..da939bc --- /dev/null +++ b/app/src/NexusContextPropagation/README.md @@ -0,0 +1,77 @@ +# Nexus Context Propagation sample + +Demonstrates copying a value from a caller workflow into the Nexus operation +headers, then reading it on the handler side. The caller writes a workflow ID +into a static `MDC` bag, a `WorkflowOutboundCallsInterceptor` copies any +`x-nexus-*` keys onto the operation's nexus headers, and the handler reads +them off `OperationContext::$headers` inside the service implementation. + +Each sample keeps its own copy of `Service/`, `Caller/` and `Handler/`. The +caller implementations mirror the [Nexus sample](../Nexus/README.md) with one +addition β€” the `MDC::put(...)` line. The `SampleNexusServiceImpl` (in +`Handler/`) logs the propagated workflow ID before delegating. + +> **Workflow-side propagation gap.** The Java sample also uses +> `MDCContextPropagator` plus a Nexus inbound interceptor to push values into +> the handler workflow's MDC so its body can log them. PHP SDK doesn't yet +> have a `ContextPropagator`, so this port logs only at the service-impl +> boundary (where `OperationContext` is available). Inside the started +> `HelloHandlerWorkflow` the headers are not visible. + +> **⚠️ Do not put secrets into Nexus headers.** Nexus header values are +> plain strings on the wire. They bypass the workflow data-converter (the +> hook used by the encryption sample to encrypt payloads end-to-end) and +> are not routed through the gRPC proxy / codec server, so anything you +> stash here will land verbatim in handler-side logs and the Temporal Web +> UI. Use them only for trace IDs, tenant IDs, correlation IDs, and other +> non-sensitive metadata. + +## Prerequisites + +Same setup as the [Nexus sample](../Nexus/README.md) β€” namespaces and the +endpoint must already exist. **Stop any other Nexus-flavour workers first**: +this sample shares `my-handler-task-queue` / `my-caller-workflow-task-queue` +and registers the same workflow type names. + +## Run + +Three terminals. + +**Handler worker** (`my-target-namespace`): + +```bash +cd app/src/NexusContextPropagation +TEMPORAL_NAMESPACE=my-target-namespace ../../rr serve -c .rr.handler.yaml +``` + +**Caller worker** (`my-caller-namespace`): + +```bash +cd app/src/NexusContextPropagation +TEMPORAL_NAMESPACE=my-caller-namespace ../../rr serve -c .rr.caller.yaml +``` + +**Starter**: + +```bash +php app/app.php nexus-context-propagation +``` + +Caller side: + +``` +Starting EchoCallerWorkflow... +Started: WorkflowID=... RunID=... +Result: Nexus Echo πŸ‘‹ + +Starting HelloCallerWorkflow... +Started: WorkflowID=... RunID=... +Result: Hello Nexus πŸ‘‹ +``` + +Handler side (in the rr stderr stream): + +``` +Echo called from a workflow with ID : +HelloHandlerWorkflow called from a workflow with ID : +``` diff --git a/app/src/NexusContextPropagation/Service/EchoInput.php b/app/src/NexusContextPropagation/Service/EchoInput.php new file mode 100644 index 0000000..fbc4a72 --- /dev/null +++ b/app/src/NexusContextPropagation/Service/EchoInput.php @@ -0,0 +1,12 @@ +newWorker( + CallerWorker::TASK_QUEUE, + interceptorProvider: new SimplePipelineProvider([ + new NexusOutboundContextInterceptor(), + ]), +)->registerWorkflowTypes( + EchoCallerWorkflowImpl::class, + HelloCallerWorkflowImpl::class, +); + +$factory->run(); diff --git a/app/src/NexusContextPropagation/handler-worker.php b/app/src/NexusContextPropagation/handler-worker.php new file mode 100644 index 0000000..a3d4711 --- /dev/null +++ b/app/src/NexusContextPropagation/handler-worker.php @@ -0,0 +1,37 @@ +withNamespace($namespace), +); + +$factory = WorkerFactory::create(client: $workflowClient); + +$worker = $factory->newWorker(HandlerWorker::TASK_QUEUE); +$worker->registerWorkflowTypes(HelloHandlerWorkflowImpl::class); +$worker->registerNexusServiceImplementation(new SampleNexusServiceImpl()); + +$factory->run(); diff --git a/app/src/NexusManualOperation/.rr.caller.yaml b/app/src/NexusManualOperation/.rr.caller.yaml new file mode 100644 index 0000000..3d46805 --- /dev/null +++ b/app/src/NexusManualOperation/.rr.caller.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6102 + +server: + command: "php caller-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-caller-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/NexusManualOperation/.rr.handler.yaml b/app/src/NexusManualOperation/.rr.handler.yaml new file mode 100644 index 0000000..36ff019 --- /dev/null +++ b/app/src/NexusManualOperation/.rr.handler.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6101 + +server: + command: "php handler-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-target-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/NexusManualOperation/Caller/CallerWorker.php b/app/src/NexusManualOperation/Caller/CallerWorker.php new file mode 100644 index 0000000..daefc1c --- /dev/null +++ b/app/src/NexusManualOperation/Caller/CallerWorker.php @@ -0,0 +1,11 @@ +stub = Workflow::newUntypedNexusOperationStub( + NexusOperationOptions::new() + ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withService('SampleNexusService') + ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)) + ->withCancellationType(NexusOperationCancellationType::TryCancel), + ); + } + + public function run(string $jobName) + { + /** @var NexusOperationHandle $handle */ + $handle = yield $this->stub->start( + 'startJob', + [new JobInput($jobName, instant: true)], + JobResult::class, + ); + + if ($handle->getOperationToken() !== null) { + throw new \LogicException('Sync result must not carry an operation token.'); + } + + /** @var JobResult $instantResult */ + $instantResult = yield $handle->getResult(); + + $handle2 = null; + $scope = Workflow::async(function () use ($jobName, &$handle2): \Generator { + $handle2 = yield $this->stub->start( + 'startJob', + [new JobInput($jobName)], + JobResult::class, + ); + yield $handle2->getResult(); + }); + + yield Workflow::await(function () use (&$handle2): bool { + return $handle2 !== null; + }); + + $token2 = $handle2->getOperationToken(); + + yield Workflow::timer(1); + $scope->cancel(); + + try { + yield $scope; + } catch (\Throwable) { + } + + return "[instant={$instantResult->message}] [token={$token2}] cancelled"; + } +} diff --git a/app/src/NexusManualOperation/ExecuteCommand.php b/app/src/NexusManualOperation/ExecuteCommand.php new file mode 100644 index 0000000..33c7322 --- /dev/null +++ b/app/src/NexusManualOperation/ExecuteCommand.php @@ -0,0 +1,53 @@ +withNamespace('my-caller-namespace'), + ); + + $output->writeln("Starting JobCallerWorkflow..."); + $workflow = $client->newWorkflowStub( + JobCallerWorkflow::class, + WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), + ); + $run = $client->start($workflow, 'Demo'); + $execution = $run->getExecution(); + $output->writeln(\sprintf( + 'Started: WorkflowID=%s RunID=%s', + $execution->getID(), + $execution->getRunID(), + )); + $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); + + return self::SUCCESS; + } +} diff --git a/app/src/NexusManualOperation/Handler/ExternalJobClient.php b/app/src/NexusManualOperation/Handler/ExternalJobClient.php new file mode 100644 index 0000000..cc9e5d1 --- /dev/null +++ b/app/src/NexusManualOperation/Handler/ExternalJobClient.php @@ -0,0 +1,18 @@ + */ +final class JobOperationHandler implements OperationHandlerInterface +{ + public function __construct( + private readonly ExternalJobClient $client, + ) {} + + public function start( + OperationContext $context, + OperationStartDetails $details, + mixed $param, + ): OperationStartResult { + if ($param->instant) { + return OperationStartResult::sync(new JobResult("done instantly: {$param->jobName}")); + } + + $jobId = $this->client->submit($param->jobName, $details->requestId); + + return OperationStartResult::async(new OperationInfo($jobId, OperationState::Running)); + } + + public function cancel( + OperationContext $context, + OperationCancelDetails $details, + ): void { + $this->client->abort($details->operationToken); + } +} diff --git a/app/src/NexusManualOperation/Handler/SampleNexusService.php b/app/src/NexusManualOperation/Handler/SampleNexusService.php new file mode 100644 index 0000000..33938ea --- /dev/null +++ b/app/src/NexusManualOperation/Handler/SampleNexusService.php @@ -0,0 +1,24 @@ +client); + } +} diff --git a/app/src/NexusManualOperation/README.md b/app/src/NexusManualOperation/README.md new file mode 100644 index 0000000..1ec7b33 --- /dev/null +++ b/app/src/NexusManualOperation/README.md @@ -0,0 +1,103 @@ +# Nexus Manual Operation sample + +The third Nexus handler form: a **manual** operation backed by an +`OperationHandlerInterface` object. Unlike `#[Operation]` (sync method) and +the `WorkflowHandle`-returning `#[AsyncOperation]` (SDK-managed workflow run), +here one object owns the whole lifecycle: + +- `start()` decides **at runtime** whether to answer synchronously + (`OperationStartResult::sync(...)` β€” no token issued) or asynchronously + (`OperationStartResult::async(new OperationInfo($token, ...))` β€” the + handler mints its **own token**, here the external job id). +- `cancel()` receives that same token back via + `$details->operationToken` and aborts the external job. + +The `#[Service]` attribute sits on a self-contained **class** β€” no interface +is needed, because the operation's wire contract (input/output types) lives +on the `#[AsyncOperation(output: ..., input: ...)]` attribute of the +zero-parameter factory method, which runs once at worker registration. No +`WorkflowClient` is needed either: nothing starts a backing workflow. + +Completion of the async result is delivered by the external system via the +Nexus completion callback (`$details->callbackUrl`) and is out of scope here β€” +the sample demonstrates the sync fast-path, the custom token, and cancel. + +The caller (`JobCallerWorkflow`) uses the **untyped** stub +(`Workflow::newUntypedNexusOperationStub()`): call 1 with `instant: true` +returns immediately with a `null` token; call 2 runs async inside a +cancellable scope, the caller reads the handler-issued `job-...` token and +then cancels the scope, which routes to the handler's `cancel()`. + +## Prerequisites + +Beyond the usual (`./temporal`, `./rr`): + +```bash +./temporal operator namespace create --namespace my-target-namespace +./temporal operator namespace create --namespace my-caller-namespace + +./temporal operator nexus endpoint create \ + --name my-nexus-endpoint-name \ + --target-namespace my-target-namespace \ + --target-task-queue my-manual-handler-task-queue +``` + +If `my-nexus-endpoint-name` already exists from the [Nexus sample](../Nexus/README.md), +re-point it instead: + +```bash +./temporal operator nexus endpoint update \ + --name my-nexus-endpoint-name \ + --target-namespace my-target-namespace \ + --target-task-queue my-manual-handler-task-queue +``` + +## Run + +Three terminals. + +**Handler worker** (`my-target-namespace`): + +```bash +cd app/src/NexusManualOperation +TEMPORAL_NAMESPACE=my-target-namespace ../../rr serve -c .rr.handler.yaml +``` + +**Caller worker** (`my-caller-namespace`): + +```bash +cd app/src/NexusManualOperation +TEMPORAL_NAMESPACE=my-caller-namespace ../../rr serve -c .rr.caller.yaml +``` + +**Starter**: + +```bash +php app/app.php nexus-manual-operation +``` + +Expected output: + +``` +Starting JobCallerWorkflow... +Started: WorkflowID=... RunID=... +Result: [instant=done instantly: Demo] [token=job-...] cancelled +``` + +The `token=job-...` part is the handler-minted token (the fake external job +id built from the start request id); the handler worker logs +`ExternalJobClient: aborted job-...` when the cancel arrives. + +## Constants + +| Name | Value | +|---|---| +| service | `SampleNexusService` | +| operations | `startJob` | +| endpoint | `my-nexus-endpoint-name` | +| handler task queue | `my-manual-handler-task-queue` | +| caller task queue | `my-manual-caller-task-queue` | +| target namespace | `my-target-namespace` | +| caller namespace | `my-caller-namespace` | +| `scheduleToCloseTimeout` | `20s` | +| `cancellationType` | `TryCancel` | diff --git a/app/src/NexusManualOperation/Service/JobInput.php b/app/src/NexusManualOperation/Service/JobInput.php new file mode 100644 index 0000000..d445a14 --- /dev/null +++ b/app/src/NexusManualOperation/Service/JobInput.php @@ -0,0 +1,13 @@ +newWorker(CallerWorker::TASK_QUEUE) + ->registerWorkflowTypes(JobCallerWorkflowImpl::class); + +$factory->run(); diff --git a/app/src/NexusManualOperation/handler-worker.php b/app/src/NexusManualOperation/handler-worker.php new file mode 100644 index 0000000..77daa93 --- /dev/null +++ b/app/src/NexusManualOperation/handler-worker.php @@ -0,0 +1,24 @@ +newWorker(HandlerWorker::TASK_QUEUE); +$worker->registerNexusServiceImplementation(new SampleNexusService()); + +$factory->run(); diff --git a/app/src/NexusMultipleArguments/.rr.caller.yaml b/app/src/NexusMultipleArguments/.rr.caller.yaml new file mode 100644 index 0000000..3d46805 --- /dev/null +++ b/app/src/NexusMultipleArguments/.rr.caller.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6102 + +server: + command: "php caller-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-caller-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/NexusMultipleArguments/.rr.handler.yaml b/app/src/NexusMultipleArguments/.rr.handler.yaml new file mode 100644 index 0000000..36ff019 --- /dev/null +++ b/app/src/NexusMultipleArguments/.rr.handler.yaml @@ -0,0 +1,16 @@ +version: "3" + +rpc: + listen: tcp://127.0.0.1:6101 + +server: + command: "php handler-worker.php" + +temporal: + address: ${TEMPORAL_HOST:-localhost}:${TEMPORAL_PORT:-7233} + namespace: ${TEMPORAL_NAMESPACE:-my-target-namespace} + activities: + num_workers: 2 + +logs: + level: info diff --git a/app/src/NexusMultipleArguments/Caller/CallerWorker.php b/app/src/NexusMultipleArguments/Caller/CallerWorker.php new file mode 100644 index 0000000..5cdd8ba --- /dev/null +++ b/app/src/NexusMultipleArguments/Caller/CallerWorker.php @@ -0,0 +1,11 @@ +sampleNexusService = Workflow::newNexusServiceStub( + SampleNexusService::class, + NexusOperationOptions::new() + ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)), + ); + } + + public function echo(string $message) + { + /** @var EchoOutput $output */ + $output = yield $this->sampleNexusService->echo(new EchoInput($message)); + return $output->message; + } +} diff --git a/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflow.php b/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflow.php new file mode 100644 index 0000000..7030663 --- /dev/null +++ b/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflow.php @@ -0,0 +1,16 @@ +sampleNexusService = Workflow::newNexusServiceStub( + SampleNexusService::class, + NexusOperationOptions::new() + ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)), + ); + } + + public function hello(string $message, Language $language) + { + /** @var HelloOutput $output */ + $output = yield $this->sampleNexusService->hello(new HelloInput($message, $language)); + return $output->message; + } +} diff --git a/app/src/NexusMultipleArguments/ExecuteCommand.php b/app/src/NexusMultipleArguments/ExecuteCommand.php new file mode 100644 index 0000000..3dabb3b --- /dev/null +++ b/app/src/NexusMultipleArguments/ExecuteCommand.php @@ -0,0 +1,69 @@ +withNamespace('my-caller-namespace'), + ); + + $output->writeln("Starting EchoCallerWorkflow..."); + $echoWorkflow = $client->newWorkflowStub( + EchoCallerWorkflow::class, + WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), + ); + $run = $client->start($echoWorkflow, 'Nexus Echo πŸ‘‹'); + $execution = $run->getExecution(); + $output->writeln(\sprintf( + 'Started: WorkflowID=%s RunID=%s', + $execution->getID(), + $execution->getRunID(), + )); + $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); + + $output->writeln("\nStarting HelloCallerWorkflow..."); + $helloWorkflow = $client->newWorkflowStub( + HelloCallerWorkflow::class, + WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), + ); + $run = $client->start($helloWorkflow, 'Nexus', Language::ES); + $execution = $run->getExecution(); + $output->writeln(\sprintf( + 'Started: WorkflowID=%s RunID=%s', + $execution->getID(), + $execution->getRunID(), + )); + $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); + + return self::SUCCESS; + } +} diff --git a/app/src/NexusMultipleArguments/Handler/EchoClient.php b/app/src/NexusMultipleArguments/Handler/EchoClient.php new file mode 100644 index 0000000..7e21efa --- /dev/null +++ b/app/src/NexusMultipleArguments/Handler/EchoClient.php @@ -0,0 +1,13 @@ +message); + } +} diff --git a/app/src/NexusMultipleArguments/Handler/HandlerWorker.php b/app/src/NexusMultipleArguments/Handler/HandlerWorker.php new file mode 100644 index 0000000..46ef999 --- /dev/null +++ b/app/src/NexusMultipleArguments/Handler/HandlerWorker.php @@ -0,0 +1,10 @@ +value}", + 'UNSUPPORTED_LANGUAGE', + false, + ); + } +} diff --git a/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php b/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php new file mode 100644 index 0000000..0c8b5a2 --- /dev/null +++ b/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php @@ -0,0 +1,38 @@ +echoClient = $echoClient ?? new EchoClientImpl(); + } + + public function echo(EchoInput $input): EchoOutput + { + return $this->echoClient->echo($input); + } + + public function hello(HelloInput $input): WorkflowHandle + { + return WorkflowHandle::fromWorkflowMethod( + HelloHandlerWorkflow::class, + WorkflowOptions::new()->withWorkflowId(Nexus::getStartDetails()->requestId), + $input->name, + $input->language, + ); + } +} diff --git a/app/src/NexusMultipleArguments/README.md b/app/src/NexusMultipleArguments/README.md new file mode 100644 index 0000000..efd5304 --- /dev/null +++ b/app/src/NexusMultipleArguments/README.md @@ -0,0 +1,121 @@ +# Nexus Multiple Arguments sample + +Same Nexus contract as the [basic Nexus sample](../Nexus/README.md), but the +backing handler workflow takes **two positional arguments** instead of one +`HelloInput` DTO. The unpack happens in the service implementation: when the +operation receives a `HelloInput`, it passes `$input->name` and +`$input->language` as separate arguments to the workflow via +`WorkflowHandle::fromWorkflowMethod(class, options, ...$args)`. + +Useful when the Nexus contract DTO and the underlying workflow signature +have to evolve independently β€” e.g. the workflow already exists with a +multi-arg signature and is called from many places, and you want to wrap +it behind a Nexus operation without changing the workflow. + +Each sample keeps its own copy of `Service/`, `Caller/` and `Handler/`; +nothing is literally shared with the [basic Nexus sample](../Nexus/README.md). +Differences: `Service/` keeps the same shape, `Handler/HelloHandlerWorkflow` +and `Handler/HelloHandlerWorkflowImpl` switch to the multi-arg signature, +and `Handler/SampleNexusServiceImpl` unpacks the input. + +## Prerequisites + +Same setup as the [basic Nexus sample](../Nexus/README.md) β€” namespaces and +the endpoint must already exist. **Stop any other Nexus-flavour workers +first**: this sample shares `my-handler-task-queue` / +`my-caller-workflow-task-queue` and registers the same workflow type names +as the basic sample but with a different signature, so co-running them +will cause deserialization failures. + +## Run + +Three terminals. + +**Handler worker** (`my-target-namespace`): + +```bash +cd app/src/NexusMultipleArguments +TEMPORAL_NAMESPACE=my-target-namespace ../../rr serve -c .rr.handler.yaml +``` + +**Caller worker** (`my-caller-namespace`): + +```bash +cd app/src/NexusMultipleArguments +TEMPORAL_NAMESPACE=my-caller-namespace ../../rr serve -c .rr.caller.yaml +``` + +**Starter**: + +```bash +php app/app.php nexus-multiple-arguments +``` + +Expected output: + +``` +Starting EchoCallerWorkflow... +Started: WorkflowID=... RunID=... +Result: Nexus Echo πŸ‘‹ + +Starting HelloCallerWorkflow... +Started: WorkflowID=... RunID=... +Result: Β‘Hola! Nexus πŸ‘‹ +``` + +## What's different from the basic sample + +Three files: the handler workflow interface and implementation (multi-arg +signature) and the service implementation (unpacks the DTO). The rest is a +copy of the basic sample. + +`Handler/HelloHandlerWorkflow.php` β€” interface signature uses positional args: + +```php +#[WorkflowInterface] +interface HelloHandlerWorkflow +{ + #[WorkflowMethod] + public function hello(string $name, Language $language); +} +``` + +`Handler/SampleNexusServiceImpl.php::hello()` β€” unpacks `HelloInput` when +building the `WorkflowHandle`: + +```php +return WorkflowHandle::fromWorkflowMethod( + HelloHandlerWorkflow::class, + WorkflowOptions::new()->withWorkflowId(Nexus::getStartDetails()->requestId), + $input->name, // ← positional + $input->language, // ← positional +); +``` + +Compare with the basic sample, which passes the whole DTO: + +```php +return WorkflowHandle::fromWorkflowMethod( + HelloHandlerWorkflow::class, + WorkflowOptions::new()->withWorkflowId(Nexus::getStartDetails()->requestId), + $input, // ← whole DTO, single arg +); +``` + +The Nexus operation contract (`SampleNexusService::hello(HelloInput): WorkflowHandle`) +and the caller workflows are unchanged. + +## Constants + +Same as the [basic Nexus sample](../Nexus/README.md). Listed here for +quick reference: + +| Name | Value | +|---|---| +| service | `SampleNexusService` | +| operations | `echo`, `hello` | +| endpoint | `my-nexus-endpoint-name` | +| handler task queue | `my-handler-task-queue` | +| caller task queue | `my-caller-workflow-task-queue` | +| target namespace | `my-target-namespace` | +| caller namespace | `my-caller-namespace` | diff --git a/app/src/NexusMultipleArguments/Service/EchoInput.php b/app/src/NexusMultipleArguments/Service/EchoInput.php new file mode 100644 index 0000000..a6155ee --- /dev/null +++ b/app/src/NexusMultipleArguments/Service/EchoInput.php @@ -0,0 +1,12 @@ +newWorker(CallerWorker::TASK_QUEUE) + ->registerWorkflowTypes( + EchoCallerWorkflowImpl::class, + HelloCallerWorkflowImpl::class, + ); + +$factory->run(); diff --git a/app/src/NexusMultipleArguments/handler-worker.php b/app/src/NexusMultipleArguments/handler-worker.php new file mode 100644 index 0000000..5f62159 --- /dev/null +++ b/app/src/NexusMultipleArguments/handler-worker.php @@ -0,0 +1,37 @@ +withNamespace($namespace), +); + +$factory = WorkerFactory::create(client: $workflowClient); + +$worker = $factory->newWorker(HandlerWorker::TASK_QUEUE); +$worker->registerWorkflowTypes(HelloHandlerWorkflowImpl::class); +$worker->registerNexusServiceImplementation(new SampleNexusServiceImpl()); + +$factory->run(); diff --git a/app/tests/Feature/.rr.test.yaml b/app/tests/Feature/.rr.test.yaml index 0cbecd4..56afacf 100644 --- a/app/tests/Feature/.rr.test.yaml +++ b/app/tests/Feature/.rr.test.yaml @@ -19,7 +19,6 @@ kv: logs: level: info - # mode: production channels: server: mode: production diff --git a/app/tests/Feature/Nexus/CallerWorkflowMockTest.php b/app/tests/Feature/Nexus/CallerWorkflowMockTest.php new file mode 100644 index 0000000..9ce1125 --- /dev/null +++ b/app/tests/Feature/Nexus/CallerWorkflowMockTest.php @@ -0,0 +1,39 @@ +newCaller(TestEchoCallerWorkflow::class); + + $result = $workflow->echo($this->endpoint['name'], 'ignored'); + + self::assertSame(MockEchoClient::CANNED, $result); + } + + public function testHelloUsesMockHandlerWorkflow(): void + { + $workflow = $this->newCaller(TestHelloCallerWorkflow::class); + + $result = $workflow->hello($this->endpoint['name'], 'World', Language::EN); + + self::assertSame(MockHelloHandlerWorkflowImpl::CANNED, $result); + } +} diff --git a/app/tests/Feature/Nexus/CallerWorkflowTest.php b/app/tests/Feature/Nexus/CallerWorkflowTest.php new file mode 100644 index 0000000..1006d71 --- /dev/null +++ b/app/tests/Feature/Nexus/CallerWorkflowTest.php @@ -0,0 +1,47 @@ +newCaller(TestEchoCallerWorkflow::class); + + $result = $workflow->echo($this->endpoint['name'], 'Hello'); + + self::assertSame('Hello', $result); + } + + public function testHelloRoundTrip(): void + { + $workflow = $this->newCaller(TestHelloCallerWorkflow::class); + + $result = $workflow->hello($this->endpoint['name'], 'World', Language::EN); + + self::assertSame('Hello World πŸ‘‹', $result); + } + + public function testHelloRespectsLanguage(): void + { + $workflow = $this->newCaller(TestHelloCallerWorkflow::class); + + $result = $workflow->hello($this->endpoint['name'], 'Nexus', Language::ES); + + self::assertSame('Β‘Hola! Nexus πŸ‘‹', $result); + } +} diff --git a/app/tests/Feature/Nexus/CancellationTest.php b/app/tests/Feature/Nexus/CancellationTest.php new file mode 100644 index 0000000..daf84bb --- /dev/null +++ b/app/tests/Feature/Nexus/CancellationTest.php @@ -0,0 +1,31 @@ +newCaller(TestCancellationCallerWorkflow::class); + + $result = $workflow->hello($this->endpoint['name'], 'Nexus'); + + self::assertContains($result, [ + 'Hello Nexus πŸ‘‹', + 'Bonjour Nexus πŸ‘‹', + 'Hallo Nexus πŸ‘‹', + 'Β‘Hola! Nexus πŸ‘‹', + 'Merhaba Nexus πŸ‘‹', + ]); + } +} diff --git a/app/tests/Feature/Nexus/ContextPropagationTest.php b/app/tests/Feature/Nexus/ContextPropagationTest.php new file mode 100644 index 0000000..bb4df88 --- /dev/null +++ b/app/tests/Feature/Nexus/ContextPropagationTest.php @@ -0,0 +1,32 @@ +newCaller( + TestContextEchoCallerWorkflow::class, + WorkflowOptions::new()->withWorkflowId($workflowId), + ); + + $result = $workflow->echo($this->endpoint['name'], 'ignored'); + + self::assertSame($workflowId, $result); + } +} diff --git a/app/tests/Feature/Nexus/HelloWithTokenTest.php b/app/tests/Feature/Nexus/HelloWithTokenTest.php new file mode 100644 index 0000000..d6ee484 --- /dev/null +++ b/app/tests/Feature/Nexus/HelloWithTokenTest.php @@ -0,0 +1,27 @@ +newCaller(TestHelloWithTokenCallerWorkflow::class); + + $result = $workflow->hello($this->endpoint['name'], 'World', Language::EN); + + self::assertNotEmpty($result['token'], 'async operation must expose an operation token'); + self::assertSame('Hello World πŸ‘‹', $result['message']); + } +} diff --git a/app/tests/Feature/Nexus/ManualOperationTest.php b/app/tests/Feature/Nexus/ManualOperationTest.php new file mode 100644 index 0000000..65ab7db --- /dev/null +++ b/app/tests/Feature/Nexus/ManualOperationTest.php @@ -0,0 +1,28 @@ +newCaller(TestManualJobCallerWorkflow::class); + + $result = $workflow->run($this->endpoint['name'], 'Demo'); + + self::assertStringContainsString('done instantly: Demo', $result); + self::assertStringContainsString('[token=job-', $result); + self::assertStringEndsWith('cancelled', $result); + } +} diff --git a/app/tests/Feature/Nexus/Mock/HeaderEchoNexusServiceImpl.php b/app/tests/Feature/Nexus/Mock/HeaderEchoNexusServiceImpl.php new file mode 100644 index 0000000..e7c87d3 --- /dev/null +++ b/app/tests/Feature/Nexus/Mock/HeaderEchoNexusServiceImpl.php @@ -0,0 +1,43 @@ +'; + + private readonly SampleNexusServiceImpl $inner; + + public function __construct() + { + $this->inner = new SampleNexusServiceImpl(); + } + + public function echo(EchoInput $input): EchoOutput + { + $headers = Nexus::getCurrentOperationContext()->headers; + + return new EchoOutput($headers->get('x-nexus-caller-workflow-id') ?? self::MISSING); + } + + public function hello(HelloInput $input): WorkflowHandle + { + return $this->inner->hello($input); + } +} diff --git a/app/tests/Feature/Nexus/Mock/MockEchoClient.php b/app/tests/Feature/Nexus/Mock/MockEchoClient.php new file mode 100644 index 0000000..b202a9e --- /dev/null +++ b/app/tests/Feature/Nexus/Mock/MockEchoClient.php @@ -0,0 +1,24 @@ +withWorkflowId(Nexus::getStartDetails()->requestId), + $input, + ); + } +} diff --git a/app/tests/Feature/Nexus/MultipleArgumentsTest.php b/app/tests/Feature/Nexus/MultipleArgumentsTest.php new file mode 100644 index 0000000..a3f9698 --- /dev/null +++ b/app/tests/Feature/Nexus/MultipleArgumentsTest.php @@ -0,0 +1,26 @@ +newCaller(TestMultiArgsHelloCallerWorkflow::class); + + $result = $workflow->hello($this->endpoint['name'], 'World', Language::ES); + + self::assertSame('Β‘Hola! World πŸ‘‹', $result); + } +} diff --git a/app/tests/Feature/Nexus/NexusEndpointHelper.php b/app/tests/Feature/Nexus/NexusEndpointHelper.php new file mode 100644 index 0000000..272aa84 --- /dev/null +++ b/app/tests/Feature/Nexus/NexusEndpointHelper.php @@ -0,0 +1,86 @@ +operator = new OperatorServiceClient( + $temporalAddress, + ['credentials' => \Grpc\ChannelCredentials::createInsecure()], + ); + } + + /** + * Create a Nexus endpoint targeting `(namespace, taskQueue)` and return + * `[id, name]`. The id is used by HTTP routes, the name is what callers + * pass to {@see \Temporal\Workflow\NexusOperationOptions::withEndpoint()}. + * + * @return array{id: string, name: string} + */ + public function setupEndpoint(string $namespace, string $taskQueue, string $prefix = 'samples-test'): array + { + $name = $prefix . '-' . \bin2hex(\random_bytes(4)); + + $request = (new CreateNexusEndpointRequest()) + ->setSpec( + (new EndpointSpec()) + ->setName($name) + ->setTarget( + (new EndpointTarget())->setWorker( + (new WorkerTarget()) + ->setNamespace($namespace) + ->setTaskQueue($taskQueue), + ), + ), + ); + + [$response, $status] = $this->operator->CreateNexusEndpoint($request)->wait(); + + if ($status->code !== \Grpc\STATUS_OK) { + throw new \RuntimeException( + "CreateNexusEndpoint failed (gRPC code {$status->code}): {$status->details}", + ); + } + + // Give the frontend's endpoint registry a moment to pick up the new + // endpoint before the first call routes through it. + \usleep(100_000); + + return ['id' => $response->getEndpoint()->getId(), 'name' => $name]; + } + + public function deleteEndpoint(string $endpointId, int $expectedVersion = 1): void + { + $request = (new DeleteNexusEndpointRequest()) + ->setId($endpointId) + ->setVersion($expectedVersion); + + [, $status] = $this->operator->DeleteNexusEndpoint($request)->wait(); + + if ($status->code !== \Grpc\STATUS_OK) { + // Best-effort cleanup; surface, don't crash other tests. + \trigger_error( + "DeleteNexusEndpoint failed (gRPC code {$status->code}): {$status->details}", + E_USER_WARNING, + ); + } + } +} diff --git a/app/tests/Feature/Nexus/NexusServiceMockTest.php b/app/tests/Feature/Nexus/NexusServiceMockTest.php new file mode 100644 index 0000000..2888c21 --- /dev/null +++ b/app/tests/Feature/Nexus/NexusServiceMockTest.php @@ -0,0 +1,38 @@ +newCaller(TestEchoCallerWorkflow::class); + + $result = $workflow->echo($this->endpoint['name'], 'ignored'); + + self::assertSame(MockSampleNexusServiceImpl::ECHO_CANNED, $result); + } + + public function testHelloFromMockedService(): void + { + $workflow = $this->newCaller(TestHelloCallerWorkflow::class); + + $result = $workflow->hello($this->endpoint['name'], 'World', Language::DE); + + self::assertSame(MockHelloHandlerWorkflowImpl::CANNED, $result); + } +} diff --git a/app/tests/Feature/Nexus/NexusTestCase.php b/app/tests/Feature/Nexus/NexusTestCase.php new file mode 100644 index 0000000..d127deb --- /dev/null +++ b/app/tests/Feature/Nexus/NexusTestCase.php @@ -0,0 +1,59 @@ +nexusHelper = new NexusEndpointHelper(\getenv('TEMPORAL_ADDRESS') ?: 'localhost:7236'); + $this->endpoint = $this->nexusHelper->setupEndpoint( + namespace: 'default', + taskQueue: static::TASK_QUEUE, + ); + } + + protected function tearDown(): void + { + $this->nexusHelper->deleteEndpoint($this->endpoint['id']); + parent::tearDown(); + } + + /** + * @template T of object + * @param class-string $workflowClass + * @return T + */ + protected function newCaller(string $workflowClass, ?WorkflowOptions $options = null): object + { + return $this->workflowClient->newWorkflowStub( + $workflowClass, + ($options ?? WorkflowOptions::new()) + ->withTaskQueue(static::TASK_QUEUE) + ->withWorkflowExecutionTimeout(CarbonInterval::seconds(60)), + ); + } +} diff --git a/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflow.php new file mode 100644 index 0000000..0b98c46 --- /dev/null +++ b/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflow.php @@ -0,0 +1,15 @@ +withEndpoint($endpoint) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)) + ->withCancellationType(NexusOperationCancellationType::WaitRequested), + ); + + $promises = []; + $scope = Workflow::async(function () use ($service, $message, &$promises): void { + foreach (Language::cases() as $language) { + $promises[] = $service->hello(new HelloInput($message, $language)); + } + }); + + /** @var HelloOutput $first */ + $first = yield Promise::any($promises); + + $scope->cancel(); + + foreach ($promises as $promise) { + try { + yield $promise; + } catch (CanceledFailure) { + } catch (NexusOperationFailure $e) { + if (!$e->getPrevious() instanceof CanceledFailure) { + throw $e; + } + } + } + + return $first->message; + } +} diff --git a/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflow.php new file mode 100644 index 0000000..6c09331 --- /dev/null +++ b/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflow.php @@ -0,0 +1,15 @@ +execution->getID()); + + /** @var SampleNexusService $service */ + $service = Workflow::newNexusServiceStub( + SampleNexusService::class, + NexusOperationOptions::new() + ->withEndpoint($endpoint) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), + ); + + /** @var EchoOutput $output */ + $output = yield $service->echo(new EchoInput($message)); + return $output->message; + } +} diff --git a/app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflow.php new file mode 100644 index 0000000..1c15f1e --- /dev/null +++ b/app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflow.php @@ -0,0 +1,20 @@ +withEndpoint($endpoint) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), + ); + /** @var EchoOutput $output */ + $output = yield $stub->echo(new EchoInput($message)); + return $output->message; + } +} diff --git a/app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflow.php new file mode 100644 index 0000000..40775b2 --- /dev/null +++ b/app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflow.php @@ -0,0 +1,16 @@ +withEndpoint($endpoint) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), + ); + /** @var HelloOutput $output */ + $output = yield $stub->hello(new HelloInput($name, $language)); + return $output->message; + } +} diff --git a/app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflow.php new file mode 100644 index 0000000..845677a --- /dev/null +++ b/app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflow.php @@ -0,0 +1,19 @@ +withEndpoint($endpoint) + ->withService('SampleNexusService') + ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), + ); + + /** @var NexusOperationHandle $handle */ + $handle = yield $stub->start( + 'hello', + [new HelloInput($name, $language)], + HelloOutput::class, + ); + + $token = $handle->getOperationToken(); + + /** @var HelloOutput $output */ + $output = yield $handle->getResult(); + + return ['token' => $token, 'message' => $output->message]; + } +} diff --git a/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflow.php new file mode 100644 index 0000000..94ba12a --- /dev/null +++ b/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflow.php @@ -0,0 +1,15 @@ +withEndpoint($endpoint) + ->withService('SampleNexusService') + ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)) + ->withCancellationType(NexusOperationCancellationType::TryCancel), + ); + + /** @var NexusOperationHandle $handle */ + $handle = yield $stub->start( + 'startJob', + [new JobInput($jobName, instant: true)], + JobResult::class, + ); + + if ($handle->getOperationToken() !== null) { + throw new \LogicException('Sync result must not carry an operation token.'); + } + + /** @var JobResult $instantResult */ + $instantResult = yield $handle->getResult(); + + $handle2 = null; + $scope = Workflow::async(static function () use ($stub, $jobName, &$handle2): \Generator { + $handle2 = yield $stub->start( + 'startJob', + [new JobInput($jobName)], + JobResult::class, + ); + yield $handle2->getResult(); + }); + + yield Workflow::await(function () use (&$handle2): bool { + return $handle2 !== null; + }); + + $token2 = $handle2->getOperationToken(); + + yield Workflow::timer(1); + $scope->cancel(); + + try { + yield $scope; + } catch (\Throwable) { + } + + return "[instant={$instantResult->message}] [token={$token2}] cancelled"; + } +} diff --git a/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php new file mode 100644 index 0000000..4911065 --- /dev/null +++ b/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php @@ -0,0 +1,16 @@ +withEndpoint($endpoint) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), + ); + + /** @var HelloOutput $output */ + $output = yield $service->hello(new HelloInput($name, $language)); + return $output->message; + } +} diff --git a/app/tests/Feature/TestCase.php b/app/tests/Feature/TestCase.php index 2418fea..4b63c69 100644 --- a/app/tests/Feature/TestCase.php +++ b/app/tests/Feature/TestCase.php @@ -15,7 +15,7 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase protected function setUp(): void { - $this->workflowClient = new WorkflowClient( + $this->workflowClient = WorkflowClient::create( ServiceClient::create(\getenv('TEMPORAL_ADDRESS')), ); diff --git a/app/tests/Feature/bootstrap.php b/app/tests/Feature/bootstrap.php index 4dd680a..70f9709 100644 --- a/app/tests/Feature/bootstrap.php +++ b/app/tests/Feature/bootstrap.php @@ -12,10 +12,21 @@ $sysInfo = \Temporal\Testing\SystemInfo::detect(); -$environment->startTemporalTestServer(); +// Nexus needs the full Temporal server; the time-skipping test server +// shipped via `startTemporalTestServer()` doesn't expose the Nexus APIs. +// Mirrors what sdk-php's acceptance harness does in TemporalStarter. +$environment->startTemporalServer( + parameters: [ + '--http-port', '7243', + ], +); +// rr's `-c` is resolved relative to its `-w` workdir, while +// Environment::startRoadRunner's internal readiness check (`rr workers -c …`) +// runs from PHP's cwd. They need different paths. $environment->startRoadRunner( - rrCommand: sprintf('%s serve -c .rr.test.yaml -w tests/Feature', $sysInfo->rrExecutable), - commandTimeout: 5 + rrCommand: [$sysInfo->rrExecutable, 'serve', '-c', '.rr.test.yaml', '-w', 'tests/Feature'], + commandTimeout: 5, + configFile: 'tests/Feature/.rr.test.yaml', ); register_shutdown_function(fn() => $environment->stop()); diff --git a/app/tests/Feature/worker.php b/app/tests/Feature/worker.php index 536ed35..9e8ca89 100644 --- a/app/tests/Feature/worker.php +++ b/app/tests/Feature/worker.php @@ -2,6 +2,30 @@ declare(strict_types=1); +use App\Tests\Feature\Nexus\CallerWorkflowMockTest; +use App\Tests\Feature\Nexus\CallerWorkflowTest; +use App\Tests\Feature\Nexus\CancellationTest; +use App\Tests\Feature\Nexus\ContextPropagationTest; +use App\Tests\Feature\Nexus\ManualOperationTest; +use App\Tests\Feature\Nexus\Mock\HeaderEchoNexusServiceImpl; +use App\Tests\Feature\Nexus\Mock\MockEchoClient; +use App\Tests\Feature\Nexus\Mock\MockHelloHandlerWorkflowImpl; +use App\Tests\Feature\Nexus\Mock\MockSampleNexusServiceImpl; +use App\Tests\Feature\Nexus\MultipleArgumentsTest; +use App\Tests\Feature\Nexus\NexusServiceMockTest; +use App\Tests\Feature\Nexus\Workflow\TestCancellationCallerWorkflowImpl; +use App\Tests\Feature\Nexus\Workflow\TestContextEchoCallerWorkflowImpl; +use App\Tests\Feature\Nexus\Workflow\TestEchoCallerWorkflowImpl; +use App\Tests\Feature\Nexus\Workflow\TestHelloCallerWorkflowImpl; +use App\Tests\Feature\Nexus\Workflow\TestHelloWithTokenCallerWorkflowImpl; +use App\Tests\Feature\Nexus\Workflow\TestManualJobCallerWorkflowImpl; +use App\Tests\Feature\Nexus\Workflow\TestMultiArgsHelloCallerWorkflowImpl; +use Temporal\Client\GRPC\ServiceClient; +use Temporal\Client\WorkflowClient; +use Temporal\Interceptor\SimplePipelineProvider; +use Temporal\Samples\Nexus\Handler\HelloHandlerWorkflowImpl; +use Temporal\Samples\Nexus\Handler\SampleNexusServiceImpl; +use Temporal\Samples\NexusContextPropagation\Propagation\NexusOutboundContextInterceptor; use Temporal\Testing\WorkerFactory; ini_set('display_errors', 'stderr'); @@ -9,15 +33,99 @@ chdir(__DIR__ . '/../..'); require_once 'vendor/autoload.php'; -$workerFactory = WorkerFactory::create(); +// Async Nexus operations (WorkflowRunOperation) start a backing workflow, so +// the worker needs a WorkflowClient threaded through to the operation context. +$workflowClient = WorkflowClient::create( + ServiceClient::create(\getenv('TEMPORAL_ADDRESS') ?: 'localhost:7236'), +); -$worker = $workerFactory->newWorker(taskQueue: 'tests'); +$workerFactory = WorkerFactory::create(client: $workflowClient); -// make sure to register concrete workflow implementations -$worker->registerWorkflowTypes(\Temporal\Samples\SimpleActivity\GreetingWorkflow::class); -$worker->registerActivity( - \Temporal\Samples\SimpleActivity\GreetingActivity::class, - fn() => new \Temporal\Samples\SimpleActivity\GreetingActivity(), -); +// Existing β€” SimpleActivity feature test. +$workerFactory->newWorker(taskQueue: 'tests') + ->registerWorkflowTypes(\Temporal\Samples\SimpleActivity\GreetingWorkflow::class) + ->registerActivity( + \Temporal\Samples\SimpleActivity\GreetingActivity::class, + fn() => new \Temporal\Samples\SimpleActivity\GreetingActivity(), + ); + +// === Nexus Feature tests === +// +// One task queue per scenario, all registered together in this single rr +// process. Each test creates its own Nexus endpoint (via gRPC OperatorService +// in setUp) targeting the matching queue, runs the test caller workflow on +// the same queue, then drops the endpoint in tearDown. + +// Scenario 1: real production handler. Mirrors `php app/app.php nexus`. +$workerFactory->newWorker(taskQueue: CallerWorkflowTest::TASK_QUEUE) + ->registerWorkflowTypes( + TestEchoCallerWorkflowImpl::class, + TestHelloCallerWorkflowImpl::class, + TestHelloWithTokenCallerWorkflowImpl::class, + HelloHandlerWorkflowImpl::class, + ) + ->registerNexusServiceImplementation(new SampleNexusServiceImpl()); + +// Scenario 2: production service-impl with the EchoClient dependency mocked, +// and a stand-in handler workflow class that returns canned data. Demonstrates +// the DI-mock + workflow-replacement test style. +$workerFactory->newWorker(taskQueue: CallerWorkflowMockTest::TASK_QUEUE) + ->registerWorkflowTypes( + TestEchoCallerWorkflowImpl::class, + TestHelloCallerWorkflowImpl::class, + MockHelloHandlerWorkflowImpl::class, + ) + ->registerNexusServiceImplementation(new SampleNexusServiceImpl(new MockEchoClient())); + +// Scenario 3: the entire Nexus service implementation is replaced. Useful +// when the production impl isn't reachable from the test (e.g. it lives in +// another package). The mock service still routes async ops through a real +// workflow start so the wire-level state machine is exercised end-to-end. +$workerFactory->newWorker(taskQueue: NexusServiceMockTest::TASK_QUEUE) + ->registerWorkflowTypes( + TestEchoCallerWorkflowImpl::class, + TestHelloCallerWorkflowImpl::class, + MockHelloHandlerWorkflowImpl::class, + ) + ->registerNexusServiceImplementation(new MockSampleNexusServiceImpl()); + +// Scenario 4: the NexusCancellation sample β€” fan-out callers + a handler +// workflow that sleeps and honours cancellation. +$workerFactory->newWorker(taskQueue: CancellationTest::TASK_QUEUE) + ->registerWorkflowTypes( + TestCancellationCallerWorkflowImpl::class, + \Temporal\Samples\NexusCancellation\Handler\HelloHandlerWorkflowImpl::class, + ) + ->registerNexusServiceImplementation(new \Temporal\Samples\NexusCancellation\Handler\SampleNexusServiceImpl()); + +// Scenario 5: the NexusContextPropagation sample β€” the production outbound +// interceptor on the caller side, a header-echoing service double on the +// handler side so the test can observe the propagated value. +$workerFactory->newWorker( + taskQueue: ContextPropagationTest::TASK_QUEUE, + interceptorProvider: new SimplePipelineProvider([ + new NexusOutboundContextInterceptor(), + ]), +) + ->registerWorkflowTypes( + TestContextEchoCallerWorkflowImpl::class, + \Temporal\Samples\NexusContextPropagation\Handler\HelloHandlerWorkflowImpl::class, + ) + ->registerNexusServiceImplementation(new HeaderEchoNexusServiceImpl()); + +// Scenario 6: the NexusMultipleArguments sample β€” single-DTO contract backed +// by a multi-argument handler workflow. +$workerFactory->newWorker(taskQueue: MultipleArgumentsTest::TASK_QUEUE) + ->registerWorkflowTypes( + TestMultiArgsHelloCallerWorkflowImpl::class, + \Temporal\Samples\NexusMultipleArguments\Handler\HelloHandlerWorkflowImpl::class, + ) + ->registerNexusServiceImplementation(new \Temporal\Samples\NexusMultipleArguments\Handler\SampleNexusServiceImpl()); + +// Scenario 7: the NexusManualOperation sample β€” a manual handler object that +// owns start (sync fast-path or async with its own token) and cancel. +$workerFactory->newWorker(taskQueue: ManualOperationTest::TASK_QUEUE) + ->registerWorkflowTypes(TestManualJobCallerWorkflowImpl::class) + ->registerNexusServiceImplementation(new \Temporal\Samples\NexusManualOperation\Handler\SampleNexusService()); $workerFactory->run(); diff --git a/app/tests/bootstrap.php b/app/tests/bootstrap.php index 46a57c1..5a185b7 100644 --- a/app/tests/bootstrap.php +++ b/app/tests/bootstrap.php @@ -17,7 +17,7 @@ # Check --filter parameter if (\preg_match('/--filter(?:=|\s++)([^"\']\S++|\'[^\']*+\'|"[^\']*+")/', $string, $matches)) { $filter = str_replace('\\\\', '\\', \trim($matches[1], '\'"')); - if (\preg_match('/Temporal\\\\Tests\\\\(\\w+)\\\\/', $filter, $matches)) { + if (\preg_match('/(?:Temporal|App)\\\\Tests\\\\(\\w+)\\\\/', $filter, $matches)) { return $matches[1]; } } From 4d17071393f63808f36c9f63aa8aab52cbd5f9bd Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Tue, 18 Aug 2026 11:47:10 +0400 Subject: [PATCH 2/5] chore: bump temporal/sdk to nexus-new head --- app/composer.lock | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/composer.lock b/app/composer.lock index 09dc8c7..42e46c5 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4514,12 +4514,12 @@ "source": { "type": "git", "url": "https://github.com/temporalio/sdk-php.git", - "reference": "d44ed80bd49a450b2c2507e8ef65bce7cb105a32" + "reference": "4bd17951195336a6d42d6323fade9bfbb642790c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/d44ed80bd49a450b2c2507e8ef65bce7cb105a32", - "reference": "d44ed80bd49a450b2c2507e8ef65bce7cb105a32", + "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/4bd17951195336a6d42d6323fade9bfbb642790c", + "reference": "4bd17951195336a6d42d6323fade9bfbb642790c", "shasum": "" }, "require": { @@ -4625,6 +4625,9 @@ "test:func": [ "tests/runner.php vendor/bin/phpunit --testsuite=Functional --color=always --testdox" ], + "test:func-timeskip": [ + "tests/runner.php vendor/bin/phpunit --testsuite=Functional-TimeSkipping --color=always --testdox" + ], "test:arch": [ "phpunit --testsuite=Arch --color=always --testdox" ], @@ -4671,7 +4674,7 @@ "issues": "https://github.com/temporalio/sdk-php/issues", "source": "https://github.com/temporalio/sdk-php" }, - "time": "2026-07-20T18:41:51+00:00" + "time": "2026-08-18T07:46:30+00:00" } ], "packages-dev": [ From d359adb1fc5b6f75521e51103cd31ff9d9526d3a Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Tue, 18 Aug 2026 19:52:21 +0400 Subject: [PATCH 3/5] refactor(nexus): mirror the Go and Java samples - share the base Service and handler contracts instead of copying them per sample - fold the operation-token observation back into HelloCallerWorkflow and drop the extra HelloWithToken workflow, command block and test - resolve the handler address like app.php does, mark UNSUPPORTED_LANGUAGE non-retryable, and give every sample its own endpoint, task queues and RR ports - surface a failed async start instead of deadlocking the manual-operation caller - make the cancellation assertions fail when cancellation stops working - create the test endpoint per class and wait until it resolves --- .gitignore | 2 + app/composer.lock | 8 +-- app/phpstan.neon.dist | 10 ++++ app/phpunit.xml.dist | 1 + .../ExceptionLoggerInterceptor.php | 34 +++++++++++ .../Nexus/Caller/HelloCallerWorkflowImpl.php | 24 ++++++-- .../Caller/HelloWithTokenCallerWorkflow.php | 16 ----- .../HelloWithTokenCallerWorkflowImpl.php | 47 --------------- app/src/Nexus/ExecuteCommand.php | 17 +----- .../Handler/HelloHandlerWorkflowImpl.php | 2 +- app/src/Nexus/README.md | 19 ++---- app/src/Nexus/caller-worker.php | 2 - app/src/Nexus/handler-worker.php | 4 +- app/src/NexusCancellation/.rr.caller.yaml | 2 +- app/src/NexusCancellation/.rr.handler.yaml | 2 +- .../NexusCancellation/Caller/CallerWorker.php | 4 +- .../Caller/HelloCallerWorkflowImpl.php | 8 +-- .../Handler/HandlerWorker.php | 2 +- .../Handler/HelloHandlerWorkflow.php | 16 ----- .../Handler/HelloHandlerWorkflowImpl.php | 9 +-- .../Handler/SampleNexusServiceImpl.php | 23 -------- app/src/NexusCancellation/README.md | 23 +++++--- .../NexusCancellation/Service/HelloInput.php | 13 ---- .../NexusCancellation/Service/HelloOutput.php | 12 ---- .../NexusCancellation/Service/Language.php | 14 ----- .../Service/SampleNexusService.php | 16 ----- app/src/NexusCancellation/handler-worker.php | 6 +- .../NexusContextPropagation/.rr.caller.yaml | 2 +- .../NexusContextPropagation/.rr.handler.yaml | 2 +- .../Caller/CallerWorker.php | 4 +- .../Caller/EchoCallerWorkflowImpl.php | 7 ++- .../Caller/HelloCallerWorkflow.php | 16 ----- .../Caller/HelloCallerWorkflowImpl.php | 9 +-- .../ExecuteCommand.php | 6 +- .../Handler/EchoClient.php | 13 ---- .../Handler/EchoClientImpl.php | 16 ----- .../Handler/HandlerWorker.php | 2 +- .../Handler/HelloHandlerWorkflow.php | 2 +- .../Handler/HelloHandlerWorkflowImpl.php | 24 +++++--- .../Handler/SampleNexusServiceImpl.php | 10 ++-- .../Propagation/MDC.php | 19 ++++-- .../NexusStartContextInterceptor.php | 50 ++++++++++++++++ .../WorkflowInboundContextInterceptor.php | 30 ++++++++++ app/src/NexusContextPropagation/README.md | 59 ++++++++++++------- .../Service/EchoInput.php | 12 ---- .../Service/EchoOutput.php | 12 ---- .../Service/HelloInput.php | 13 ---- .../Service/HelloOutput.php | 12 ---- .../Service/Language.php | 14 ----- .../Service/SampleNexusService.php | 20 ------- .../handler-worker.php | 13 +++- app/src/NexusManualOperation/.rr.caller.yaml | 2 +- app/src/NexusManualOperation/.rr.handler.yaml | 2 +- .../Caller/CallerWorker.php | 2 +- .../Caller/JobCallerWorkflowImpl.php | 44 +++++++++++--- .../Handler/ExternalJobClient.php | 2 +- app/src/NexusManualOperation/README.md | 15 ++--- .../NexusMultipleArguments/.rr.caller.yaml | 2 +- .../NexusMultipleArguments/.rr.handler.yaml | 2 +- .../Caller/CallerWorker.php | 4 +- .../Caller/EchoCallerWorkflowImpl.php | 6 +- .../Caller/HelloCallerWorkflow.php | 2 +- .../Caller/HelloCallerWorkflowImpl.php | 8 +-- .../NexusMultipleArguments/ExecuteCommand.php | 2 +- .../Handler/EchoClient.php | 13 ---- .../Handler/EchoClientImpl.php | 16 ----- .../Handler/HandlerWorker.php | 2 +- .../Handler/HelloHandlerWorkflow.php | 2 +- .../Handler/HelloHandlerWorkflowImpl.php | 6 +- .../Handler/SampleNexusServiceImpl.php | 10 ++-- app/src/NexusMultipleArguments/README.md | 33 ++++++----- .../Service/EchoInput.php | 12 ---- .../Service/EchoOutput.php | 12 ---- .../Service/HelloInput.php | 13 ---- .../Service/HelloOutput.php | 12 ---- .../Service/Language.php | 14 ----- .../Service/SampleNexusService.php | 20 ------- .../NexusMultipleArguments/handler-worker.php | 4 +- app/tests/Feature/Nexus/CancellationTest.php | 3 +- .../Feature/Nexus/ContextPropagationTest.php | 14 +++++ .../Feature/Nexus/HelloWithTokenTest.php | 27 --------- .../Nexus/ManualOperationStartFailureTest.php | 38 ++++++++++++ .../Feature/Nexus/Mock/FailingJobClient.php | 19 ++++++ .../Nexus/Mock/HeaderEchoNexusServiceImpl.php | 8 +-- .../Feature/Nexus/MultipleArgumentsTest.php | 2 +- .../Feature/Nexus/NexusEndpointHelper.php | 33 +++++++++-- app/tests/Feature/Nexus/NexusTestCase.php | 43 ++++++++++---- .../TestCancellationCallerWorkflowImpl.php | 15 +++-- .../TestContextEchoCallerWorkflowImpl.php | 6 +- .../TestContextHelloCallerWorkflow.php} | 6 +- .../TestContextHelloCallerWorkflowImpl.php | 34 +++++++++++ .../TestHelloWithTokenCallerWorkflow.php | 19 ------ .../TestHelloWithTokenCallerWorkflowImpl.php | 41 ------------- .../TestManualJobCallerWorkflowImpl.php | 44 +++++++++++--- .../TestMultiArgsHelloCallerWorkflow.php | 2 +- .../TestMultiArgsHelloCallerWorkflowImpl.php | 8 +-- app/tests/Feature/bootstrap.php | 4 +- app/tests/Feature/worker.php | 20 ++++++- 98 files changed, 630 insertions(+), 721 deletions(-) create mode 100644 app/phpstan.neon.dist create mode 100644 app/src/Interceptors/Interceptor/ExceptionLoggerInterceptor.php delete mode 100644 app/src/Nexus/Caller/HelloWithTokenCallerWorkflow.php delete mode 100644 app/src/Nexus/Caller/HelloWithTokenCallerWorkflowImpl.php delete mode 100644 app/src/NexusCancellation/Handler/HelloHandlerWorkflow.php delete mode 100644 app/src/NexusCancellation/Handler/SampleNexusServiceImpl.php delete mode 100644 app/src/NexusCancellation/Service/HelloInput.php delete mode 100644 app/src/NexusCancellation/Service/HelloOutput.php delete mode 100644 app/src/NexusCancellation/Service/Language.php delete mode 100644 app/src/NexusCancellation/Service/SampleNexusService.php delete mode 100644 app/src/NexusContextPropagation/Caller/HelloCallerWorkflow.php delete mode 100644 app/src/NexusContextPropagation/Handler/EchoClient.php delete mode 100644 app/src/NexusContextPropagation/Handler/EchoClientImpl.php create mode 100644 app/src/NexusContextPropagation/Propagation/NexusStartContextInterceptor.php create mode 100644 app/src/NexusContextPropagation/Propagation/WorkflowInboundContextInterceptor.php delete mode 100644 app/src/NexusContextPropagation/Service/EchoInput.php delete mode 100644 app/src/NexusContextPropagation/Service/EchoOutput.php delete mode 100644 app/src/NexusContextPropagation/Service/HelloInput.php delete mode 100644 app/src/NexusContextPropagation/Service/HelloOutput.php delete mode 100644 app/src/NexusContextPropagation/Service/Language.php delete mode 100644 app/src/NexusContextPropagation/Service/SampleNexusService.php delete mode 100644 app/src/NexusMultipleArguments/Handler/EchoClient.php delete mode 100644 app/src/NexusMultipleArguments/Handler/EchoClientImpl.php delete mode 100644 app/src/NexusMultipleArguments/Service/EchoInput.php delete mode 100644 app/src/NexusMultipleArguments/Service/EchoOutput.php delete mode 100644 app/src/NexusMultipleArguments/Service/HelloInput.php delete mode 100644 app/src/NexusMultipleArguments/Service/HelloOutput.php delete mode 100644 app/src/NexusMultipleArguments/Service/Language.php delete mode 100644 app/src/NexusMultipleArguments/Service/SampleNexusService.php delete mode 100644 app/tests/Feature/Nexus/HelloWithTokenTest.php create mode 100644 app/tests/Feature/Nexus/ManualOperationStartFailureTest.php create mode 100644 app/tests/Feature/Nexus/Mock/FailingJobClient.php rename app/{src/NexusContextPropagation/Caller/EchoCallerWorkflow.php => tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflow.php} (52%) create mode 100644 app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflowImpl.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflow.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflowImpl.php diff --git a/.gitignore b/.gitignore index d8ebdcc..18849f0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ app/**/temporal.exe app/tests/**/*.log app/runtime app/vendor +app/**/temporal-test-server +.DS_Store diff --git a/app/composer.lock b/app/composer.lock index 42e46c5..4c5a985 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4514,12 +4514,12 @@ "source": { "type": "git", "url": "https://github.com/temporalio/sdk-php.git", - "reference": "4bd17951195336a6d42d6323fade9bfbb642790c" + "reference": "85126b125b82c5218c70d9b96a1d8bbdbafd00b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/4bd17951195336a6d42d6323fade9bfbb642790c", - "reference": "4bd17951195336a6d42d6323fade9bfbb642790c", + "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/85126b125b82c5218c70d9b96a1d8bbdbafd00b7", + "reference": "85126b125b82c5218c70d9b96a1d8bbdbafd00b7", "shasum": "" }, "require": { @@ -4674,7 +4674,7 @@ "issues": "https://github.com/temporalio/sdk-php/issues", "source": "https://github.com/temporalio/sdk-php" }, - "time": "2026-08-18T07:46:30+00:00" + "time": "2026-08-18T15:44:58+00:00" } ], "packages-dev": [ diff --git a/app/phpstan.neon.dist b/app/phpstan.neon.dist new file mode 100644 index 0000000..ca8aa31 --- /dev/null +++ b/app/phpstan.neon.dist @@ -0,0 +1,10 @@ +parameters: + level: 6 + paths: + - src/Nexus + - src/NexusCancellation + - src/NexusContextPropagation + - src/NexusMultipleArguments + - tests/Feature + excludePaths: + - tests/Feature/worker.log diff --git a/app/phpunit.xml.dist b/app/phpunit.xml.dist index 0f5e939..497fc38 100644 --- a/app/phpunit.xml.dist +++ b/app/phpunit.xml.dist @@ -19,6 +19,7 @@ + diff --git a/app/src/Interceptors/Interceptor/ExceptionLoggerInterceptor.php b/app/src/Interceptors/Interceptor/ExceptionLoggerInterceptor.php new file mode 100644 index 0000000..32c94d5 --- /dev/null +++ b/app/src/Interceptors/Interceptor/ExceptionLoggerInterceptor.php @@ -0,0 +1,34 @@ +logger->error($e->getMessage(), ['exception' => $e]); + throw $e; + } + } +} diff --git a/app/src/Nexus/Caller/HelloCallerWorkflowImpl.php b/app/src/Nexus/Caller/HelloCallerWorkflowImpl.php index a5ddbcb..94e0371 100644 --- a/app/src/Nexus/Caller/HelloCallerWorkflowImpl.php +++ b/app/src/Nexus/Caller/HelloCallerWorkflowImpl.php @@ -8,29 +8,41 @@ use Temporal\Samples\Nexus\Service\HelloInput; use Temporal\Samples\Nexus\Service\HelloOutput; use Temporal\Samples\Nexus\Service\Language; -use Temporal\Samples\Nexus\Service\SampleNexusService; use Temporal\Workflow; +use Temporal\Workflow\NexusOperationHandle; use Temporal\Workflow\NexusOperationOptions; +use Temporal\Workflow\NexusOperationStubInterface; class HelloCallerWorkflowImpl implements HelloCallerWorkflow { - /** @var SampleNexusService */ - private object $sampleNexusService; + private NexusOperationStubInterface $sampleNexusService; public function __construct() { - $this->sampleNexusService = Workflow::newNexusServiceStub( - SampleNexusService::class, + $this->sampleNexusService = Workflow::newUntypedNexusOperationStub( NexusOperationOptions::new() ->withEndpoint(CallerWorker::ENDPOINT_NAME) + ->withService('SampleNexusService') ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)), ); } public function hello(string $message, Language $language) { + /** @var NexusOperationHandle $handle */ + $handle = yield $this->sampleNexusService->start( + 'hello', + [new HelloInput($message, $language)], + HelloOutput::class, + ); + + Workflow::getLogger()->info( + 'Nexus operation started, token: ' . ($handle->getOperationToken() ?? ''), + ); + /** @var HelloOutput $output */ - $output = yield $this->sampleNexusService->hello(new HelloInput($message, $language)); + $output = yield $handle->getResult(); + return $output->message; } } diff --git a/app/src/Nexus/Caller/HelloWithTokenCallerWorkflow.php b/app/src/Nexus/Caller/HelloWithTokenCallerWorkflow.php deleted file mode 100644 index e20bb80..0000000 --- a/app/src/Nexus/Caller/HelloWithTokenCallerWorkflow.php +++ /dev/null @@ -1,16 +0,0 @@ -stub = Workflow::newUntypedNexusOperationStub( - NexusOperationOptions::new() - ->withEndpoint(CallerWorker::ENDPOINT_NAME) - ->withService('SampleNexusService') - ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)), - ); - } - - public function hello(string $message, Language $language) - { - /** @var NexusOperationHandle $handle */ - $handle = yield $this->stub->start( - 'hello', - [new HelloInput($message, $language)], - HelloOutput::class, - ); - - $token = $handle->getOperationToken() ?? ''; - Workflow::getLogger()->info("Nexus operation started, token: {$token}"); - - /** @var HelloOutput $output */ - $output = yield $handle->getResult(); - - return "[token={$token}] {$output->message}"; - } -} diff --git a/app/src/Nexus/ExecuteCommand.php b/app/src/Nexus/ExecuteCommand.php index a543f8c..e8c9b3b 100644 --- a/app/src/Nexus/ExecuteCommand.php +++ b/app/src/Nexus/ExecuteCommand.php @@ -20,14 +20,13 @@ use Temporal\Samples\Nexus\Caller\CallerWorker; use Temporal\Samples\Nexus\Caller\EchoCallerWorkflow; use Temporal\Samples\Nexus\Caller\HelloCallerWorkflow; -use Temporal\Samples\Nexus\Caller\HelloWithTokenCallerWorkflow; use Temporal\Samples\Nexus\Service\Language; use Temporal\SampleUtils\Command; class ExecuteCommand extends Command { protected const NAME = 'nexus'; - protected const DESCRIPTION = 'Execute Nexus\\EchoCallerWorkflow + HelloCallerWorkflow + HelloWithTokenCallerWorkflow (cross-namespace via Nexus)'; + protected const DESCRIPTION = 'Execute Nexus\\EchoCallerWorkflow + HelloCallerWorkflow (cross-namespace via Nexus)'; public function execute(InputInterface $input, OutputInterface $output): int { @@ -65,20 +64,6 @@ public function execute(InputInterface $input, OutputInterface $output): int )); $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); - $output->writeln("\nStarting HelloWithTokenCallerWorkflow..."); - $tokenWorkflow = $client->newWorkflowStub( - HelloWithTokenCallerWorkflow::class, - WorkflowOptions::new()->withTaskQueue(CallerWorker::TASK_QUEUE), - ); - $run = $client->start($tokenWorkflow, 'Nexus', Language::FR); - $execution = $run->getExecution(); - $output->writeln(\sprintf( - 'Started: WorkflowID=%s RunID=%s', - $execution->getID(), - $execution->getRunID(), - )); - $output->writeln(\sprintf("Result: %s", $run->getResult('string'))); - return self::SUCCESS; } } diff --git a/app/src/Nexus/Handler/HelloHandlerWorkflowImpl.php b/app/src/Nexus/Handler/HelloHandlerWorkflowImpl.php index 0453fb4..08ed102 100644 --- a/app/src/Nexus/Handler/HelloHandlerWorkflowImpl.php +++ b/app/src/Nexus/Handler/HelloHandlerWorkflowImpl.php @@ -28,7 +28,7 @@ public function hello(HelloInput $input): HelloOutput throw new ApplicationFailure( "Unsupported language: {$input->language->value}", 'UNSUPPORTED_LANGUAGE', - false, + true, ); } } diff --git a/app/src/Nexus/README.md b/app/src/Nexus/README.md index ae6d4e1..7403634 100644 --- a/app/src/Nexus/README.md +++ b/app/src/Nexus/README.md @@ -7,16 +7,13 @@ service hosted by a handler worker in `my-target-namespace`. Two operations: - `hello` β€” async, `WorkflowRunOperation`, starts `HelloHandlerWorkflow` server-side (`#[AsyncOperation(output: HelloOutput::class)]`) -Three caller workflows: +Two caller workflows: - `EchoCallerWorkflow` β€” typed stub, sync op. -- `HelloCallerWorkflow` β€” typed stub, async op (the typed proxy resolves - directly to `HelloOutput`, the operation token stays hidden). -- `HelloWithTokenCallerWorkflow` β€” same call routed through the **untyped** - stub via `Workflow::newUntypedNexusOperationStub()`. `start()` returns a +- `HelloCallerWorkflow` β€” untyped stub via + `Workflow::newUntypedNexusOperationStub()`. `start()` resolves with a `NexusOperationHandle` that exposes `operationToken` (string for async, - null for sync) before `getResult()` resolves with the typed result. Use - this when you need the token β€” for cancel-by-token, linking, logging. + null for sync) before `getResult()` resolves with the typed result. ## Prerequisites @@ -67,16 +64,8 @@ Result: Nexus Echo πŸ‘‹ Starting HelloCallerWorkflow... Started: WorkflowID=... RunID=... Result: Β‘Hola! Nexus πŸ‘‹ - -Starting HelloWithTokenCallerWorkflow... -Started: WorkflowID=... RunID=... -Result: [token=ChJteS10YXJnZXQt...] Bonjour Nexus πŸ‘‹ ``` -The `token=...` prefix in the third line is the server-issued operation -token, base64-ish blob encoding `namespace + workflowId`. It is only -present for async operations. - ## Constants | Name | Value | diff --git a/app/src/Nexus/caller-worker.php b/app/src/Nexus/caller-worker.php index e4be46d..d17758a 100644 --- a/app/src/Nexus/caller-worker.php +++ b/app/src/Nexus/caller-worker.php @@ -12,7 +12,6 @@ use Temporal\Samples\Nexus\Caller\CallerWorker; use Temporal\Samples\Nexus\Caller\EchoCallerWorkflowImpl; use Temporal\Samples\Nexus\Caller\HelloCallerWorkflowImpl; -use Temporal\Samples\Nexus\Caller\HelloWithTokenCallerWorkflowImpl; use Temporal\WorkerFactory; ini_set('display_errors', 'stderr'); @@ -24,7 +23,6 @@ ->registerWorkflowTypes( EchoCallerWorkflowImpl::class, HelloCallerWorkflowImpl::class, - HelloWithTokenCallerWorkflowImpl::class, ); $factory->run(); diff --git a/app/src/Nexus/handler-worker.php b/app/src/Nexus/handler-worker.php index f2c1ad6..ed1c30b 100644 --- a/app/src/Nexus/handler-worker.php +++ b/app/src/Nexus/handler-worker.php @@ -20,7 +20,9 @@ ini_set('display_errors', 'stderr'); include "../../vendor/autoload.php"; -$address = \getenv('TEMPORAL_ADDRESS') ?: '127.0.0.1:7233'; +$address = \getenv('TEMPORAL_ADDRESS') + ?: \getenv('TEMPORAL_CLI_ADDRESS') + ?: ((\getenv('TEMPORAL_HOST') ?: '127.0.0.1') . ':' . (\getenv('TEMPORAL_PORT') ?: '7233')); $namespace = \getenv('TEMPORAL_NAMESPACE') ?: 'my-target-namespace'; $workflowClient = WorkflowClient::create( diff --git a/app/src/NexusCancellation/.rr.caller.yaml b/app/src/NexusCancellation/.rr.caller.yaml index 3d46805..ddbec40 100644 --- a/app/src/NexusCancellation/.rr.caller.yaml +++ b/app/src/NexusCancellation/.rr.caller.yaml @@ -1,7 +1,7 @@ version: "3" rpc: - listen: tcp://127.0.0.1:6102 + listen: tcp://127.0.0.1:6112 server: command: "php caller-worker.php" diff --git a/app/src/NexusCancellation/.rr.handler.yaml b/app/src/NexusCancellation/.rr.handler.yaml index 36ff019..60d43d8 100644 --- a/app/src/NexusCancellation/.rr.handler.yaml +++ b/app/src/NexusCancellation/.rr.handler.yaml @@ -1,7 +1,7 @@ version: "3" rpc: - listen: tcp://127.0.0.1:6101 + listen: tcp://127.0.0.1:6111 server: command: "php handler-worker.php" diff --git a/app/src/NexusCancellation/Caller/CallerWorker.php b/app/src/NexusCancellation/Caller/CallerWorker.php index 73ec213..b2236cb 100644 --- a/app/src/NexusCancellation/Caller/CallerWorker.php +++ b/app/src/NexusCancellation/Caller/CallerWorker.php @@ -6,6 +6,6 @@ final class CallerWorker { - public const TASK_QUEUE = 'my-caller-workflow-task-queue'; - public const ENDPOINT_NAME = 'my-nexus-endpoint-name'; + public const TASK_QUEUE = 'my-cancellation-caller-task-queue'; + public const ENDPOINT_NAME = 'my-cancellation-nexus-endpoint'; } diff --git a/app/src/NexusCancellation/Caller/HelloCallerWorkflowImpl.php b/app/src/NexusCancellation/Caller/HelloCallerWorkflowImpl.php index 109b4b9..b7a65b8 100644 --- a/app/src/NexusCancellation/Caller/HelloCallerWorkflowImpl.php +++ b/app/src/NexusCancellation/Caller/HelloCallerWorkflowImpl.php @@ -8,10 +8,10 @@ use Temporal\Exception\Failure\CanceledFailure; use Temporal\Exception\Failure\NexusOperationFailure; use Temporal\Promise; -use Temporal\Samples\NexusCancellation\Service\HelloInput; -use Temporal\Samples\NexusCancellation\Service\HelloOutput; -use Temporal\Samples\NexusCancellation\Service\Language; -use Temporal\Samples\NexusCancellation\Service\SampleNexusService; +use Temporal\Samples\Nexus\Service\HelloInput; +use Temporal\Samples\Nexus\Service\HelloOutput; +use Temporal\Samples\Nexus\Service\Language; +use Temporal\Samples\Nexus\Service\SampleNexusService; use Temporal\Workflow; use Temporal\Workflow\NexusOperationCancellationType; use Temporal\Workflow\NexusOperationOptions; diff --git a/app/src/NexusCancellation/Handler/HandlerWorker.php b/app/src/NexusCancellation/Handler/HandlerWorker.php index d43ad2b..a1fce96 100644 --- a/app/src/NexusCancellation/Handler/HandlerWorker.php +++ b/app/src/NexusCancellation/Handler/HandlerWorker.php @@ -6,5 +6,5 @@ final class HandlerWorker { - public const TASK_QUEUE = 'my-handler-task-queue'; + public const TASK_QUEUE = 'my-cancellation-handler-task-queue'; } diff --git a/app/src/NexusCancellation/Handler/HelloHandlerWorkflow.php b/app/src/NexusCancellation/Handler/HelloHandlerWorkflow.php deleted file mode 100644 index fd6da36..0000000 --- a/app/src/NexusCancellation/Handler/HelloHandlerWorkflow.php +++ /dev/null @@ -1,16 +0,0 @@ -language->value}", 'UNSUPPORTED_LANGUAGE', - false, + true, ); } catch (CanceledFailure $e) { yield Workflow::asyncDetached(function () { diff --git a/app/src/NexusCancellation/Handler/SampleNexusServiceImpl.php b/app/src/NexusCancellation/Handler/SampleNexusServiceImpl.php deleted file mode 100644 index 7359fcf..0000000 --- a/app/src/NexusCancellation/Handler/SampleNexusServiceImpl.php +++ /dev/null @@ -1,23 +0,0 @@ -withWorkflowId(Nexus::getStartDetails()->requestId), - $input, - ); - } -} diff --git a/app/src/NexusCancellation/README.md b/app/src/NexusCancellation/README.md index 8fc9da8..2fa27bf 100644 --- a/app/src/NexusCancellation/README.md +++ b/app/src/NexusCancellation/README.md @@ -6,9 +6,10 @@ the other 4 in-flight operations get cancellation requests, the handler workflows clean up and rethrow `CanceledFailure`, and the caller drains the promises (ignoring `CanceledFailure`) before returning the first result. -Defines its own single-operation `SampleNexusService` (no `echo`), modeled -on the [Nexus sample](../Nexus/README.md). Its `HelloHandlerWorkflowImpl` -(in `Handler/`) adds a random delay and a detached cleanup scope. +Reuses the service contract and `SampleNexusServiceImpl` of the +[Nexus sample](../Nexus/README.md); the only sample-specific handler code is +`HelloHandlerWorkflowImpl`, which adds a random delay and a detached cleanup +scope. Caller passes `NexusOperationCancellationType::WaitRequested` so it returns as soon as the handler acknowledges the cancellation request β€” it doesn't @@ -16,10 +17,18 @@ wait for the detached cleanup scope to finish on the handler side. ## Prerequisites -Same setup as the [Nexus sample](../Nexus/README.md) β€” namespaces and the -endpoint must already exist. **Stop any regular-Nexus workers first**: both -samples share `my-handler-task-queue` / `my-caller-workflow-task-queue` and -register the same workflow type names. +Beyond the usual (`./temporal`, `./rr`), and the two namespaces the +[Nexus sample](../Nexus/README.md) creates: + +```bash +./temporal operator nexus endpoint create \ + --name my-cancellation-nexus-endpoint \ + --target-namespace my-target-namespace \ + --target-task-queue my-cancellation-handler-task-queue +``` + +This sample owns its endpoint, task queues and RoadRunner RPC ports, so it +can run alongside the other Nexus samples. ## Run diff --git a/app/src/NexusCancellation/Service/HelloInput.php b/app/src/NexusCancellation/Service/HelloInput.php deleted file mode 100644 index 0f86464..0000000 --- a/app/src/NexusCancellation/Service/HelloInput.php +++ /dev/null @@ -1,13 +0,0 @@ -message); - } -} diff --git a/app/src/NexusContextPropagation/Handler/HandlerWorker.php b/app/src/NexusContextPropagation/Handler/HandlerWorker.php index fd2285d..226ef22 100644 --- a/app/src/NexusContextPropagation/Handler/HandlerWorker.php +++ b/app/src/NexusContextPropagation/Handler/HandlerWorker.php @@ -6,5 +6,5 @@ final class HandlerWorker { - public const TASK_QUEUE = 'my-handler-task-queue'; + public const TASK_QUEUE = 'my-context-propagation-handler-task-queue'; } diff --git a/app/src/NexusContextPropagation/Handler/HelloHandlerWorkflow.php b/app/src/NexusContextPropagation/Handler/HelloHandlerWorkflow.php index 6d584b5..00c7275 100644 --- a/app/src/NexusContextPropagation/Handler/HelloHandlerWorkflow.php +++ b/app/src/NexusContextPropagation/Handler/HelloHandlerWorkflow.php @@ -4,7 +4,7 @@ namespace Temporal\Samples\NexusContextPropagation\Handler; -use Temporal\Samples\NexusContextPropagation\Service\HelloInput; +use Temporal\Samples\Nexus\Service\HelloInput; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; diff --git a/app/src/NexusContextPropagation/Handler/HelloHandlerWorkflowImpl.php b/app/src/NexusContextPropagation/Handler/HelloHandlerWorkflowImpl.php index 0b15f8c..2b3e78f 100644 --- a/app/src/NexusContextPropagation/Handler/HelloHandlerWorkflowImpl.php +++ b/app/src/NexusContextPropagation/Handler/HelloHandlerWorkflowImpl.php @@ -5,30 +5,36 @@ namespace Temporal\Samples\NexusContextPropagation\Handler; use Temporal\Exception\Failure\ApplicationFailure; -use Temporal\Samples\NexusContextPropagation\Service\HelloInput; -use Temporal\Samples\NexusContextPropagation\Service\HelloOutput; -use Temporal\Samples\NexusContextPropagation\Service\Language; +use Temporal\Samples\Nexus\Service\HelloInput; +use Temporal\Samples\Nexus\Service\HelloOutput; +use Temporal\Samples\NexusContextPropagation\Propagation\MDC; +use Temporal\Samples\Nexus\Service\Language; class HelloHandlerWorkflowImpl implements HelloHandlerWorkflow { public function hello(HelloInput $input): HelloOutput { + $callerWorkflowId = MDC::get('x-nexus-caller-workflow-id'); + $name = $callerWorkflowId === null + ? $input->name + : $input->name . ', x-nexus-caller-workflow-id: ' . $callerWorkflowId; + switch ($input->language) { case Language::EN: - return new HelloOutput("Hello {$input->name} πŸ‘‹"); + return new HelloOutput("Hello {$name} πŸ‘‹"); case Language::FR: - return new HelloOutput("Bonjour {$input->name} πŸ‘‹"); + return new HelloOutput("Bonjour {$name} πŸ‘‹"); case Language::DE: - return new HelloOutput("Hallo {$input->name} πŸ‘‹"); + return new HelloOutput("Hallo {$name} πŸ‘‹"); case Language::ES: - return new HelloOutput("Β‘Hola! {$input->name} πŸ‘‹"); + return new HelloOutput("Β‘Hola! {$name} πŸ‘‹"); case Language::TR: - return new HelloOutput("Merhaba {$input->name} πŸ‘‹"); + return new HelloOutput("Merhaba {$name} πŸ‘‹"); } throw new ApplicationFailure( "Unsupported language: {$input->language->value}", 'UNSUPPORTED_LANGUAGE', - false, + true, ); } } diff --git a/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php b/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php index 79c26df..ec22e62 100644 --- a/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php +++ b/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php @@ -7,10 +7,12 @@ use Temporal\Client\WorkflowOptions; use Temporal\Nexus\Nexus; use Temporal\Nexus\WorkflowHandle; -use Temporal\Samples\NexusContextPropagation\Service\EchoInput; -use Temporal\Samples\NexusContextPropagation\Service\EchoOutput; -use Temporal\Samples\NexusContextPropagation\Service\HelloInput; -use Temporal\Samples\NexusContextPropagation\Service\SampleNexusService; +use Temporal\Samples\Nexus\Handler\EchoClient; +use Temporal\Samples\Nexus\Handler\EchoClientImpl; +use Temporal\Samples\Nexus\Service\EchoInput; +use Temporal\Samples\Nexus\Service\EchoOutput; +use Temporal\Samples\Nexus\Service\HelloInput; +use Temporal\Samples\Nexus\Service\SampleNexusService; final class SampleNexusServiceImpl implements SampleNexusService { diff --git a/app/src/NexusContextPropagation/Propagation/MDC.php b/app/src/NexusContextPropagation/Propagation/MDC.php index 0174c43..e8cc3c2 100644 --- a/app/src/NexusContextPropagation/Propagation/MDC.php +++ b/app/src/NexusContextPropagation/Propagation/MDC.php @@ -4,29 +4,36 @@ namespace Temporal\Samples\NexusContextPropagation\Propagation; +use Temporal\Workflow; + final class MDC { - /** @var array */ - private static array $context = []; + /** @var array> */ + private static array $contexts = []; public static function put(string $key, string $value): void { - self::$context[$key] = $value; + self::$contexts[self::scope()][$key] = $value; } public static function get(string $key): ?string { - return self::$context[$key] ?? null; + return self::$contexts[self::scope()][$key] ?? null; } /** @return array */ public static function getAll(): array { - return self::$context; + return self::$contexts[self::scope()] ?? []; } public static function clear(): void { - self::$context = []; + unset(self::$contexts[self::scope()]); + } + + private static function scope(): string + { + return Workflow::getInfo()->execution->getID(); } } diff --git a/app/src/NexusContextPropagation/Propagation/NexusStartContextInterceptor.php b/app/src/NexusContextPropagation/Propagation/NexusStartContextInterceptor.php new file mode 100644 index 0000000..d958268 --- /dev/null +++ b/app/src/NexusContextPropagation/Propagation/NexusStartContextInterceptor.php @@ -0,0 +1,50 @@ +with(header: Header::fromValues($propagated))); + } + + /** + * @return array + */ + private static function propagatedHeaders(): array + { + try { + $headers = Nexus::getCurrentOperationContext()->headers->all(); + } catch (\LogicException) { + return []; + } + + $propagated = []; + foreach ($headers as $name => $value) { + if (\str_starts_with($name, self::NEXUS_HEADER_PREFIX)) { + $propagated[$name] = $value; + } + } + + return $propagated; + } +} diff --git a/app/src/NexusContextPropagation/Propagation/WorkflowInboundContextInterceptor.php b/app/src/NexusContextPropagation/Propagation/WorkflowInboundContextInterceptor.php new file mode 100644 index 0000000..a3dc6d3 --- /dev/null +++ b/app/src/NexusContextPropagation/Propagation/WorkflowInboundContextInterceptor.php @@ -0,0 +1,30 @@ +header as $name => $_) { + $name = (string) $name; + if (\str_starts_with($name, self::NEXUS_HEADER_PREFIX)) { + MDC::put($name, (string) $input->header->getValue($name, 'string')); + } + } + + $next($input); + } +} diff --git a/app/src/NexusContextPropagation/README.md b/app/src/NexusContextPropagation/README.md index da939bc..609119d 100644 --- a/app/src/NexusContextPropagation/README.md +++ b/app/src/NexusContextPropagation/README.md @@ -1,22 +1,31 @@ # Nexus Context Propagation sample -Demonstrates copying a value from a caller workflow into the Nexus operation -headers, then reading it on the handler side. The caller writes a workflow ID -into a static `MDC` bag, a `WorkflowOutboundCallsInterceptor` copies any -`x-nexus-*` keys onto the operation's nexus headers, and the handler reads -them off `OperationContext::$headers` inside the service implementation. - -Each sample keeps its own copy of `Service/`, `Caller/` and `Handler/`. The -caller implementations mirror the [Nexus sample](../Nexus/README.md) with one -addition β€” the `MDC::put(...)` line. The `SampleNexusServiceImpl` (in -`Handler/`) logs the propagated workflow ID before delegating. - -> **Workflow-side propagation gap.** The Java sample also uses -> `MDCContextPropagator` plus a Nexus inbound interceptor to push values into -> the handler workflow's MDC so its body can log them. PHP SDK doesn't yet -> have a `ContextPropagator`, so this port logs only at the service-impl -> boundary (where `OperationContext` is available). Inside the started -> `HelloHandlerWorkflow` the headers are not visible. +Demonstrates carrying a value from a caller workflow all the way into the +workflow that backs an async Nexus operation. Four pieces, all in +`Propagation/`: + +| Piece | Role | +|---|---| +| `MDC` | per-workflow bag of `x-nexus-*` values | +| `NexusOutboundContextInterceptor` | caller side: MDC β†’ operation headers | +| `NexusStartContextInterceptor` | handler side: operation headers β†’ start header of the backing workflow | +| `WorkflowInboundContextInterceptor` | backing workflow: start header β†’ MDC | + +`NexusStartContextInterceptor` is a `WorkflowClientCallsInterceptor` registered +on the handler worker's `WorkflowClient`, which is the client the SDK uses to +start the backing workflow β€” the same shape as `ContextPropagator` in the Java +and Go samples. Nothing is propagated unless you register it: the SDK never +copies Nexus headers into a workflow by itself. + +The service contract, the caller workflow interfaces and the handler +`EchoClient` come from the [Nexus sample](../Nexus/README.md); only +`Handler/HelloHandlerWorkflow` stays local, so this sample's handler workflow +can be registered next to the base one in the shared feature-test worker. The +caller +implementations mirror it with one addition β€” the `MDC::put(...)` line. +`SampleNexusServiceImpl` logs the +propagated workflow ID, and `HelloHandlerWorkflowImpl` appends it to the +greeting, so the value is observable at both boundaries. > **⚠️ Do not put secrets into Nexus headers.** Nexus header values are > plain strings on the wire. They bypass the workflow data-converter (the @@ -28,10 +37,18 @@ addition β€” the `MDC::put(...)` line. The `SampleNexusServiceImpl` (in ## Prerequisites -Same setup as the [Nexus sample](../Nexus/README.md) β€” namespaces and the -endpoint must already exist. **Stop any other Nexus-flavour workers first**: -this sample shares `my-handler-task-queue` / `my-caller-workflow-task-queue` -and registers the same workflow type names. +Beyond the usual (`./temporal`, `./rr`), and the two namespaces the +[Nexus sample](../Nexus/README.md) creates: + +```bash +./temporal operator nexus endpoint create \ + --name my-context-propagation-nexus-endpoint \ + --target-namespace my-target-namespace \ + --target-task-queue my-context-propagation-handler-task-queue +``` + +This sample owns its endpoint, task queues and RoadRunner RPC ports, so it +can run alongside the other Nexus samples. ## Run diff --git a/app/src/NexusContextPropagation/Service/EchoInput.php b/app/src/NexusContextPropagation/Service/EchoInput.php deleted file mode 100644 index fbc4a72..0000000 --- a/app/src/NexusContextPropagation/Service/EchoInput.php +++ /dev/null @@ -1,12 +0,0 @@ -withNamespace($namespace), + interceptorProvider: new SimplePipelineProvider([new NexusStartContextInterceptor()]), ); $factory = WorkerFactory::create(client: $workflowClient); -$worker = $factory->newWorker(HandlerWorker::TASK_QUEUE); +$worker = $factory->newWorker( + HandlerWorker::TASK_QUEUE, + interceptorProvider: new SimplePipelineProvider([new WorkflowInboundContextInterceptor()]), +); $worker->registerWorkflowTypes(HelloHandlerWorkflowImpl::class); $worker->registerNexusServiceImplementation(new SampleNexusServiceImpl()); diff --git a/app/src/NexusManualOperation/.rr.caller.yaml b/app/src/NexusManualOperation/.rr.caller.yaml index 3d46805..bd2aa62 100644 --- a/app/src/NexusManualOperation/.rr.caller.yaml +++ b/app/src/NexusManualOperation/.rr.caller.yaml @@ -1,7 +1,7 @@ version: "3" rpc: - listen: tcp://127.0.0.1:6102 + listen: tcp://127.0.0.1:6142 server: command: "php caller-worker.php" diff --git a/app/src/NexusManualOperation/.rr.handler.yaml b/app/src/NexusManualOperation/.rr.handler.yaml index 36ff019..1319e79 100644 --- a/app/src/NexusManualOperation/.rr.handler.yaml +++ b/app/src/NexusManualOperation/.rr.handler.yaml @@ -1,7 +1,7 @@ version: "3" rpc: - listen: tcp://127.0.0.1:6101 + listen: tcp://127.0.0.1:6141 server: command: "php handler-worker.php" diff --git a/app/src/NexusManualOperation/Caller/CallerWorker.php b/app/src/NexusManualOperation/Caller/CallerWorker.php index daefc1c..6be1a11 100644 --- a/app/src/NexusManualOperation/Caller/CallerWorker.php +++ b/app/src/NexusManualOperation/Caller/CallerWorker.php @@ -7,5 +7,5 @@ final class CallerWorker { public const TASK_QUEUE = 'my-manual-caller-task-queue'; - public const ENDPOINT_NAME = 'my-nexus-endpoint-name'; + public const ENDPOINT_NAME = 'my-manual-nexus-endpoint'; } diff --git a/app/src/NexusManualOperation/Caller/JobCallerWorkflowImpl.php b/app/src/NexusManualOperation/Caller/JobCallerWorkflowImpl.php index 4bfbe5b..b5552f0 100644 --- a/app/src/NexusManualOperation/Caller/JobCallerWorkflowImpl.php +++ b/app/src/NexusManualOperation/Caller/JobCallerWorkflowImpl.php @@ -5,6 +5,8 @@ namespace Temporal\Samples\NexusManualOperation\Caller; use Carbon\CarbonInterval; +use Temporal\Exception\Failure\CanceledFailure; +use Temporal\Exception\Failure\NexusOperationFailure; use Temporal\Samples\NexusManualOperation\Service\JobInput; use Temporal\Samples\NexusManualOperation\Service\JobResult; use Temporal\Workflow; @@ -45,27 +47,51 @@ public function run(string $jobName) $instantResult = yield $handle->getResult(); $handle2 = null; - $scope = Workflow::async(function () use ($jobName, &$handle2): \Generator { - $handle2 = yield $this->stub->start( - 'startJob', - [new JobInput($jobName)], - JobResult::class, - ); + $failure = null; + $scope = Workflow::async(function () use ($jobName, &$handle2, &$failure): \Generator { + try { + $handle2 = yield $this->stub->start( + 'startJob', + [new JobInput($jobName)], + JobResult::class, + ); + } catch (\Throwable $e) { + $failure = $e; + throw $e; + } + yield $handle2->getResult(); }); - yield Workflow::await(function () use (&$handle2): bool { - return $handle2 !== null; + yield Workflow::await(function () use (&$handle2, &$failure): bool { + return $handle2 !== null || $failure !== null; }); + if ($failure !== null) { + throw $failure; + } + $token2 = $handle2->getOperationToken(); yield Workflow::timer(1); $scope->cancel(); + $cancelled = false; + try { yield $scope; - } catch (\Throwable) { + } catch (CanceledFailure) { + $cancelled = true; + } catch (NexusOperationFailure $e) { + if (!$e->getPrevious() instanceof CanceledFailure) { + throw $e; + } + + $cancelled = true; + } + + if (!$cancelled) { + throw new \LogicException('Async operation did not end cancelled.'); } return "[instant={$instantResult->message}] [token={$token2}] cancelled"; diff --git a/app/src/NexusManualOperation/Handler/ExternalJobClient.php b/app/src/NexusManualOperation/Handler/ExternalJobClient.php index cc9e5d1..8186a04 100644 --- a/app/src/NexusManualOperation/Handler/ExternalJobClient.php +++ b/app/src/NexusManualOperation/Handler/ExternalJobClient.php @@ -4,7 +4,7 @@ namespace Temporal\Samples\NexusManualOperation\Handler; -final class ExternalJobClient +class ExternalJobClient { public function submit(string $jobName, string $requestId): string { diff --git a/app/src/NexusManualOperation/README.md b/app/src/NexusManualOperation/README.md index 1ec7b33..a2bdbd1 100644 --- a/app/src/NexusManualOperation/README.md +++ b/app/src/NexusManualOperation/README.md @@ -37,20 +37,13 @@ Beyond the usual (`./temporal`, `./rr`): ./temporal operator namespace create --namespace my-caller-namespace ./temporal operator nexus endpoint create \ - --name my-nexus-endpoint-name \ + --name my-manual-nexus-endpoint \ --target-namespace my-target-namespace \ --target-task-queue my-manual-handler-task-queue ``` -If `my-nexus-endpoint-name` already exists from the [Nexus sample](../Nexus/README.md), -re-point it instead: - -```bash -./temporal operator nexus endpoint update \ - --name my-nexus-endpoint-name \ - --target-namespace my-target-namespace \ - --target-task-queue my-manual-handler-task-queue -``` +This sample owns its endpoint, task queues and RoadRunner RPC ports, so it +can run alongside the other Nexus samples. ## Run @@ -94,7 +87,7 @@ id built from the start request id); the handler worker logs |---|---| | service | `SampleNexusService` | | operations | `startJob` | -| endpoint | `my-nexus-endpoint-name` | +| endpoint | `my-manual-nexus-endpoint` | | handler task queue | `my-manual-handler-task-queue` | | caller task queue | `my-manual-caller-task-queue` | | target namespace | `my-target-namespace` | diff --git a/app/src/NexusMultipleArguments/.rr.caller.yaml b/app/src/NexusMultipleArguments/.rr.caller.yaml index 3d46805..d82b5cc 100644 --- a/app/src/NexusMultipleArguments/.rr.caller.yaml +++ b/app/src/NexusMultipleArguments/.rr.caller.yaml @@ -1,7 +1,7 @@ version: "3" rpc: - listen: tcp://127.0.0.1:6102 + listen: tcp://127.0.0.1:6132 server: command: "php caller-worker.php" diff --git a/app/src/NexusMultipleArguments/.rr.handler.yaml b/app/src/NexusMultipleArguments/.rr.handler.yaml index 36ff019..ac98e47 100644 --- a/app/src/NexusMultipleArguments/.rr.handler.yaml +++ b/app/src/NexusMultipleArguments/.rr.handler.yaml @@ -1,7 +1,7 @@ version: "3" rpc: - listen: tcp://127.0.0.1:6101 + listen: tcp://127.0.0.1:6131 server: command: "php handler-worker.php" diff --git a/app/src/NexusMultipleArguments/Caller/CallerWorker.php b/app/src/NexusMultipleArguments/Caller/CallerWorker.php index 5cdd8ba..0eccfb7 100644 --- a/app/src/NexusMultipleArguments/Caller/CallerWorker.php +++ b/app/src/NexusMultipleArguments/Caller/CallerWorker.php @@ -6,6 +6,6 @@ final class CallerWorker { - public const TASK_QUEUE = 'my-caller-workflow-task-queue'; - public const ENDPOINT_NAME = 'my-nexus-endpoint-name'; + public const TASK_QUEUE = 'my-multiple-arguments-caller-task-queue'; + public const ENDPOINT_NAME = 'my-multiple-arguments-nexus-endpoint'; } diff --git a/app/src/NexusMultipleArguments/Caller/EchoCallerWorkflowImpl.php b/app/src/NexusMultipleArguments/Caller/EchoCallerWorkflowImpl.php index 5a31202..9e88c4d 100644 --- a/app/src/NexusMultipleArguments/Caller/EchoCallerWorkflowImpl.php +++ b/app/src/NexusMultipleArguments/Caller/EchoCallerWorkflowImpl.php @@ -5,9 +5,9 @@ namespace Temporal\Samples\NexusMultipleArguments\Caller; use Carbon\CarbonInterval; -use Temporal\Samples\NexusMultipleArguments\Service\EchoInput; -use Temporal\Samples\NexusMultipleArguments\Service\EchoOutput; -use Temporal\Samples\NexusMultipleArguments\Service\SampleNexusService; +use Temporal\Samples\Nexus\Service\EchoInput; +use Temporal\Samples\Nexus\Service\EchoOutput; +use Temporal\Samples\Nexus\Service\SampleNexusService; use Temporal\Workflow; use Temporal\Workflow\NexusOperationOptions; diff --git a/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflow.php b/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflow.php index 7030663..bf47c5e 100644 --- a/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflow.php +++ b/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflow.php @@ -4,7 +4,7 @@ namespace Temporal\Samples\NexusMultipleArguments\Caller; -use Temporal\Samples\NexusMultipleArguments\Service\Language; +use Temporal\Samples\Nexus\Service\Language; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; diff --git a/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflowImpl.php b/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflowImpl.php index 6309540..3c57733 100644 --- a/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflowImpl.php +++ b/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflowImpl.php @@ -5,10 +5,10 @@ namespace Temporal\Samples\NexusMultipleArguments\Caller; use Carbon\CarbonInterval; -use Temporal\Samples\NexusMultipleArguments\Service\HelloInput; -use Temporal\Samples\NexusMultipleArguments\Service\HelloOutput; -use Temporal\Samples\NexusMultipleArguments\Service\Language; -use Temporal\Samples\NexusMultipleArguments\Service\SampleNexusService; +use Temporal\Samples\Nexus\Service\HelloInput; +use Temporal\Samples\Nexus\Service\HelloOutput; +use Temporal\Samples\Nexus\Service\Language; +use Temporal\Samples\Nexus\Service\SampleNexusService; use Temporal\Workflow; use Temporal\Workflow\NexusOperationOptions; diff --git a/app/src/NexusMultipleArguments/ExecuteCommand.php b/app/src/NexusMultipleArguments/ExecuteCommand.php index 3dabb3b..9cdd320 100644 --- a/app/src/NexusMultipleArguments/ExecuteCommand.php +++ b/app/src/NexusMultipleArguments/ExecuteCommand.php @@ -20,7 +20,7 @@ use Temporal\Samples\NexusMultipleArguments\Caller\CallerWorker; use Temporal\Samples\NexusMultipleArguments\Caller\EchoCallerWorkflow; use Temporal\Samples\NexusMultipleArguments\Caller\HelloCallerWorkflow; -use Temporal\Samples\NexusMultipleArguments\Service\Language; +use Temporal\Samples\Nexus\Service\Language; use Temporal\SampleUtils\Command; class ExecuteCommand extends Command diff --git a/app/src/NexusMultipleArguments/Handler/EchoClient.php b/app/src/NexusMultipleArguments/Handler/EchoClient.php deleted file mode 100644 index 7e21efa..0000000 --- a/app/src/NexusMultipleArguments/Handler/EchoClient.php +++ /dev/null @@ -1,13 +0,0 @@ -message); - } -} diff --git a/app/src/NexusMultipleArguments/Handler/HandlerWorker.php b/app/src/NexusMultipleArguments/Handler/HandlerWorker.php index 46ef999..e3e63c6 100644 --- a/app/src/NexusMultipleArguments/Handler/HandlerWorker.php +++ b/app/src/NexusMultipleArguments/Handler/HandlerWorker.php @@ -6,5 +6,5 @@ final class HandlerWorker { - public const TASK_QUEUE = 'my-handler-task-queue'; + public const TASK_QUEUE = 'my-multiple-arguments-handler-task-queue'; } diff --git a/app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflow.php b/app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflow.php index c79161d..657423c 100644 --- a/app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflow.php +++ b/app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflow.php @@ -4,7 +4,7 @@ namespace Temporal\Samples\NexusMultipleArguments\Handler; -use Temporal\Samples\NexusMultipleArguments\Service\Language; +use Temporal\Samples\Nexus\Service\Language; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; diff --git a/app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflowImpl.php b/app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflowImpl.php index e7f432e..bc54929 100644 --- a/app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflowImpl.php +++ b/app/src/NexusMultipleArguments/Handler/HelloHandlerWorkflowImpl.php @@ -5,8 +5,8 @@ namespace Temporal\Samples\NexusMultipleArguments\Handler; use Temporal\Exception\Failure\ApplicationFailure; -use Temporal\Samples\NexusMultipleArguments\Service\HelloOutput; -use Temporal\Samples\NexusMultipleArguments\Service\Language; +use Temporal\Samples\Nexus\Service\HelloOutput; +use Temporal\Samples\Nexus\Service\Language; class HelloHandlerWorkflowImpl implements HelloHandlerWorkflow { @@ -27,7 +27,7 @@ public function hello(string $name, Language $language): HelloOutput throw new ApplicationFailure( "Unsupported language: {$language->value}", 'UNSUPPORTED_LANGUAGE', - false, + true, ); } } diff --git a/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php b/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php index 0c8b5a2..d237de8 100644 --- a/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php +++ b/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php @@ -7,10 +7,12 @@ use Temporal\Client\WorkflowOptions; use Temporal\Nexus\Nexus; use Temporal\Nexus\WorkflowHandle; -use Temporal\Samples\NexusMultipleArguments\Service\EchoInput; -use Temporal\Samples\NexusMultipleArguments\Service\EchoOutput; -use Temporal\Samples\NexusMultipleArguments\Service\HelloInput; -use Temporal\Samples\NexusMultipleArguments\Service\SampleNexusService; +use Temporal\Samples\Nexus\Handler\EchoClient; +use Temporal\Samples\Nexus\Handler\EchoClientImpl; +use Temporal\Samples\Nexus\Service\EchoInput; +use Temporal\Samples\Nexus\Service\EchoOutput; +use Temporal\Samples\Nexus\Service\HelloInput; +use Temporal\Samples\Nexus\Service\SampleNexusService; final class SampleNexusServiceImpl implements SampleNexusService { diff --git a/app/src/NexusMultipleArguments/README.md b/app/src/NexusMultipleArguments/README.md index efd5304..982fd4c 100644 --- a/app/src/NexusMultipleArguments/README.md +++ b/app/src/NexusMultipleArguments/README.md @@ -12,20 +12,25 @@ have to evolve independently β€” e.g. the workflow already exists with a multi-arg signature and is called from many places, and you want to wrap it behind a Nexus operation without changing the workflow. -Each sample keeps its own copy of `Service/`, `Caller/` and `Handler/`; -nothing is literally shared with the [basic Nexus sample](../Nexus/README.md). -Differences: `Service/` keeps the same shape, `Handler/HelloHandlerWorkflow` -and `Handler/HelloHandlerWorkflowImpl` switch to the multi-arg signature, -and `Handler/SampleNexusServiceImpl` unpacks the input. +The service contract is imported from the +[basic Nexus sample](../Nexus/README.md). Only `Handler/HelloHandlerWorkflow` +and `Handler/HelloHandlerWorkflowImpl` (multi-arg signature) and +`Handler/SampleNexusServiceImpl` (unpacks the input) are sample-specific. ## Prerequisites -Same setup as the [basic Nexus sample](../Nexus/README.md) β€” namespaces and -the endpoint must already exist. **Stop any other Nexus-flavour workers -first**: this sample shares `my-handler-task-queue` / -`my-caller-workflow-task-queue` and registers the same workflow type names -as the basic sample but with a different signature, so co-running them -will cause deserialization failures. +Beyond the usual (`./temporal`, `./rr`), and the two namespaces the +[Nexus sample](../Nexus/README.md) creates: + +```bash +./temporal operator nexus endpoint create \ + --name my-multiple-arguments-nexus-endpoint \ + --target-namespace my-target-namespace \ + --target-task-queue my-multiple-arguments-handler-task-queue +``` + +This sample owns its endpoint, task queues and RoadRunner RPC ports, so it +can run alongside the other Nexus samples. ## Run @@ -114,8 +119,8 @@ quick reference: |---|---| | service | `SampleNexusService` | | operations | `echo`, `hello` | -| endpoint | `my-nexus-endpoint-name` | -| handler task queue | `my-handler-task-queue` | -| caller task queue | `my-caller-workflow-task-queue` | +| endpoint | `my-multiple-arguments-nexus-endpoint` | +| handler task queue | `my-multiple-arguments-handler-task-queue` | +| caller task queue | `my-multiple-arguments-caller-task-queue` | | target namespace | `my-target-namespace` | | caller namespace | `my-caller-namespace` | diff --git a/app/src/NexusMultipleArguments/Service/EchoInput.php b/app/src/NexusMultipleArguments/Service/EchoInput.php deleted file mode 100644 index a6155ee..0000000 --- a/app/src/NexusMultipleArguments/Service/EchoInput.php +++ /dev/null @@ -1,12 +0,0 @@ -hello($this->endpoint['name'], 'Nexus'); - self::assertContains($result, [ + self::assertStringEndsWith(' [cancelled=4]', $result); + self::assertContains(\substr($result, 0, -\strlen(' [cancelled=4]')), [ 'Hello Nexus πŸ‘‹', 'Bonjour Nexus πŸ‘‹', 'Hallo Nexus πŸ‘‹', diff --git a/app/tests/Feature/Nexus/ContextPropagationTest.php b/app/tests/Feature/Nexus/ContextPropagationTest.php index bb4df88..94366b1 100644 --- a/app/tests/Feature/Nexus/ContextPropagationTest.php +++ b/app/tests/Feature/Nexus/ContextPropagationTest.php @@ -5,6 +5,7 @@ namespace App\Tests\Feature\Nexus; use App\Tests\Feature\Nexus\Workflow\TestContextEchoCallerWorkflow; +use App\Tests\Feature\Nexus\Workflow\TestContextHelloCallerWorkflow; use Temporal\Client\WorkflowOptions; /** @@ -29,4 +30,17 @@ public function testCallerWorkflowIdPropagatesViaHeaders(): void self::assertSame($workflowId, $result); } + + public function testCallerWorkflowIdReachesTheBackingWorkflow(): void + { + $workflowId = 'ctx-prop-async-' . \bin2hex(\random_bytes(4)); + $workflow = $this->newCaller( + TestContextHelloCallerWorkflow::class, + WorkflowOptions::new()->withWorkflowId($workflowId), + ); + + $result = $workflow->hello($this->endpoint['name'], 'Nexus'); + + self::assertStringContainsString($workflowId, $result); + } } diff --git a/app/tests/Feature/Nexus/HelloWithTokenTest.php b/app/tests/Feature/Nexus/HelloWithTokenTest.php deleted file mode 100644 index d6ee484..0000000 --- a/app/tests/Feature/Nexus/HelloWithTokenTest.php +++ /dev/null @@ -1,27 +0,0 @@ -newCaller(TestHelloWithTokenCallerWorkflow::class); - - $result = $workflow->hello($this->endpoint['name'], 'World', Language::EN); - - self::assertNotEmpty($result['token'], 'async operation must expose an operation token'); - self::assertSame('Hello World πŸ‘‹', $result['message']); - } -} diff --git a/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php b/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php new file mode 100644 index 0000000..138a189 --- /dev/null +++ b/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php @@ -0,0 +1,38 @@ +newCaller( + TestManualJobCallerWorkflow::class, + WorkflowOptions::new()->withWorkflowExecutionTimeout(CarbonInterval::seconds(15)), + ); + + try { + $workflow->run($this->endpoint['name'], 'Demo'); + self::fail('Expected the workflow to fail with the handler error.'); + } catch (WorkflowFailedException $e) { + self::assertStringContainsString( + FailingJobClient::FAILURE_MESSAGE, + (string) $e, + ); + } + } +} diff --git a/app/tests/Feature/Nexus/Mock/FailingJobClient.php b/app/tests/Feature/Nexus/Mock/FailingJobClient.php new file mode 100644 index 0000000..dbc7f32 --- /dev/null +++ b/app/tests/Feature/Nexus/Mock/FailingJobClient.php @@ -0,0 +1,19 @@ +getEndpoint()->getId(); + $this->awaitEndpointResolvable($id); - return ['id' => $response->getEndpoint()->getId(), 'name' => $name]; + return ['id' => $id, 'name' => $name]; + } + + private function awaitEndpointResolvable(string $endpointId, float $timeoutSeconds = 15.0): void + { + $deadline = \microtime(true) + $timeoutSeconds; + $request = (new GetNexusEndpointRequest())->setId($endpointId); + + do { + [, $status] = $this->operator->GetNexusEndpoint($request)->wait(); + + if ($status->code === \Grpc\STATUS_OK) { + return; + } + + \usleep(100_000); + } while (\microtime(true) < $deadline); + + throw new \RuntimeException( + "Nexus endpoint {$endpointId} did not become resolvable within {$timeoutSeconds}s.", + ); + } + + public function close(): void + { + $this->operator->close(); } public function deleteEndpoint(string $endpointId, int $expectedVersion = 1): void diff --git a/app/tests/Feature/Nexus/NexusTestCase.php b/app/tests/Feature/Nexus/NexusTestCase.php index d127deb..c928f93 100644 --- a/app/tests/Feature/Nexus/NexusTestCase.php +++ b/app/tests/Feature/Nexus/NexusTestCase.php @@ -10,36 +10,56 @@ /** * Base for Nexus feature tests: creates a dedicated Nexus endpoint targeting - * `static::TASK_QUEUE` before each test and drops it afterwards. + * `static::TASK_QUEUE` once per test class and drops it afterwards. */ abstract class NexusTestCase extends TestCase { public const TASK_QUEUE = ''; - protected NexusEndpointHelper $nexusHelper; + private static ?NexusEndpointHelper $nexusHelper = null; /** @var array{id: string, name: string} */ protected array $endpoint; - protected function setUp(): void + /** @var array{id: string, name: string}|null */ + private static ?array $sharedEndpoint = null; + + public static function setUpBeforeClass(): void { - parent::setUp(); + parent::setUpBeforeClass(); if (static::TASK_QUEUE === '') { self::fail(\sprintf('%s must override the TASK_QUEUE constant.', static::class)); } - $this->nexusHelper = new NexusEndpointHelper(\getenv('TEMPORAL_ADDRESS') ?: 'localhost:7236'); - $this->endpoint = $this->nexusHelper->setupEndpoint( + self::$nexusHelper = new NexusEndpointHelper(\getenv('TEMPORAL_ADDRESS') ?: 'localhost:7236'); + self::$sharedEndpoint = self::$nexusHelper->setupEndpoint( namespace: 'default', taskQueue: static::TASK_QUEUE, ); } - protected function tearDown(): void + public static function tearDownAfterClass(): void { - $this->nexusHelper->deleteEndpoint($this->endpoint['id']); - parent::tearDown(); + if (self::$nexusHelper !== null) { + if (self::$sharedEndpoint !== null) { + self::$nexusHelper->deleteEndpoint(self::$sharedEndpoint['id']); + } + + self::$nexusHelper->close(); + } + + self::$nexusHelper = null; + self::$sharedEndpoint = null; + + parent::tearDownAfterClass(); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->endpoint = self::$sharedEndpoint; } /** @@ -51,9 +71,8 @@ protected function newCaller(string $workflowClass, ?WorkflowOptions $options = { return $this->workflowClient->newWorkflowStub( $workflowClass, - ($options ?? WorkflowOptions::new()) - ->withTaskQueue(static::TASK_QUEUE) - ->withWorkflowExecutionTimeout(CarbonInterval::seconds(60)), + ($options ?? WorkflowOptions::new()->withWorkflowExecutionTimeout(CarbonInterval::seconds(60))) + ->withTaskQueue(static::TASK_QUEUE), ); } } diff --git a/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflowImpl.php b/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflowImpl.php index f59ec75..5e5f7fc 100644 --- a/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflowImpl.php +++ b/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflowImpl.php @@ -8,10 +8,10 @@ use Temporal\Exception\Failure\CanceledFailure; use Temporal\Exception\Failure\NexusOperationFailure; use Temporal\Promise; -use Temporal\Samples\NexusCancellation\Service\HelloInput; -use Temporal\Samples\NexusCancellation\Service\HelloOutput; -use Temporal\Samples\NexusCancellation\Service\Language; -use Temporal\Samples\NexusCancellation\Service\SampleNexusService; +use Temporal\Samples\Nexus\Service\HelloInput; +use Temporal\Samples\Nexus\Service\HelloOutput; +use Temporal\Samples\Nexus\Service\Language; +use Temporal\Samples\Nexus\Service\SampleNexusService; use Temporal\Workflow; use Temporal\Workflow\NexusOperationCancellationType; use Temporal\Workflow\NexusOperationOptions; @@ -45,17 +45,22 @@ public function hello(string $endpoint, string $message) $scope->cancel(); + $cancelled = 0; + foreach ($promises as $promise) { try { yield $promise; } catch (CanceledFailure) { + ++$cancelled; } catch (NexusOperationFailure $e) { if (!$e->getPrevious() instanceof CanceledFailure) { throw $e; } + + ++$cancelled; } } - return $first->message; + return "{$first->message} [cancelled={$cancelled}]"; } } diff --git a/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflowImpl.php b/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflowImpl.php index 443fd42..b47f078 100644 --- a/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflowImpl.php +++ b/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflowImpl.php @@ -6,9 +6,9 @@ use Carbon\CarbonInterval; use Temporal\Samples\NexusContextPropagation\Propagation\MDC; -use Temporal\Samples\NexusContextPropagation\Service\EchoInput; -use Temporal\Samples\NexusContextPropagation\Service\EchoOutput; -use Temporal\Samples\NexusContextPropagation\Service\SampleNexusService; +use Temporal\Samples\Nexus\Service\EchoInput; +use Temporal\Samples\Nexus\Service\EchoOutput; +use Temporal\Samples\Nexus\Service\SampleNexusService; use Temporal\Workflow; use Temporal\Workflow\NexusOperationOptions; diff --git a/app/src/NexusContextPropagation/Caller/EchoCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflow.php similarity index 52% rename from app/src/NexusContextPropagation/Caller/EchoCallerWorkflow.php rename to app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflow.php index ce775f6..95f7034 100644 --- a/app/src/NexusContextPropagation/Caller/EchoCallerWorkflow.php +++ b/app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflow.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Temporal\Samples\NexusContextPropagation\Caller; +namespace App\Tests\Feature\Nexus\Workflow; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; #[WorkflowInterface] -interface EchoCallerWorkflow +interface TestContextHelloCallerWorkflow { #[WorkflowMethod] - public function echo(string $message); + public function hello(string $endpoint, string $name); } diff --git a/app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflowImpl.php b/app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflowImpl.php new file mode 100644 index 0000000..8425d0a --- /dev/null +++ b/app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflowImpl.php @@ -0,0 +1,34 @@ +execution->getID()); + + /** @var SampleNexusService $service */ + $service = Workflow::newNexusServiceStub( + SampleNexusService::class, + NexusOperationOptions::new() + ->withEndpoint($endpoint) + ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), + ); + + /** @var HelloOutput $output */ + $output = yield $service->hello(new HelloInput($name, Language::EN)); + return $output->message; + } +} diff --git a/app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflow.php deleted file mode 100644 index 845677a..0000000 --- a/app/tests/Feature/Nexus/Workflow/TestHelloWithTokenCallerWorkflow.php +++ /dev/null @@ -1,19 +0,0 @@ -withEndpoint($endpoint) - ->withService('SampleNexusService') - ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), - ); - - /** @var NexusOperationHandle $handle */ - $handle = yield $stub->start( - 'hello', - [new HelloInput($name, $language)], - HelloOutput::class, - ); - - $token = $handle->getOperationToken(); - - /** @var HelloOutput $output */ - $output = yield $handle->getResult(); - - return ['token' => $token, 'message' => $output->message]; - } -} diff --git a/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflowImpl.php b/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflowImpl.php index 8a6be93..b65489b 100644 --- a/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflowImpl.php +++ b/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflowImpl.php @@ -5,6 +5,8 @@ namespace App\Tests\Feature\Nexus\Workflow; use Carbon\CarbonInterval; +use Temporal\Exception\Failure\CanceledFailure; +use Temporal\Exception\Failure\NexusOperationFailure; use Temporal\Samples\NexusManualOperation\Service\JobInput; use Temporal\Samples\NexusManualOperation\Service\JobResult; use Temporal\Workflow; @@ -44,27 +46,51 @@ public function run(string $endpoint, string $jobName) $instantResult = yield $handle->getResult(); $handle2 = null; - $scope = Workflow::async(static function () use ($stub, $jobName, &$handle2): \Generator { - $handle2 = yield $stub->start( - 'startJob', - [new JobInput($jobName)], - JobResult::class, - ); + $failure = null; + $scope = Workflow::async(static function () use ($stub, $jobName, &$handle2, &$failure): \Generator { + try { + $handle2 = yield $stub->start( + 'startJob', + [new JobInput($jobName)], + JobResult::class, + ); + } catch (\Throwable $e) { + $failure = $e; + throw $e; + } + yield $handle2->getResult(); }); - yield Workflow::await(function () use (&$handle2): bool { - return $handle2 !== null; + yield Workflow::await(function () use (&$handle2, &$failure): bool { + return $handle2 !== null || $failure !== null; }); + if ($failure !== null) { + throw $failure; + } + $token2 = $handle2->getOperationToken(); yield Workflow::timer(1); $scope->cancel(); + $cancelled = false; + try { yield $scope; - } catch (\Throwable) { + } catch (CanceledFailure) { + $cancelled = true; + } catch (NexusOperationFailure $e) { + if (!$e->getPrevious() instanceof CanceledFailure) { + throw $e; + } + + $cancelled = true; + } + + if (!$cancelled) { + throw new \LogicException('Async operation did not end cancelled.'); } return "[instant={$instantResult->message}] [token={$token2}] cancelled"; diff --git a/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php index 4911065..5ad476d 100644 --- a/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php +++ b/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php @@ -4,7 +4,7 @@ namespace App\Tests\Feature\Nexus\Workflow; -use Temporal\Samples\NexusMultipleArguments\Service\Language; +use Temporal\Samples\Nexus\Service\Language; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; diff --git a/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflowImpl.php b/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflowImpl.php index d51cd7d..507d25c 100644 --- a/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflowImpl.php +++ b/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflowImpl.php @@ -5,10 +5,10 @@ namespace App\Tests\Feature\Nexus\Workflow; use Carbon\CarbonInterval; -use Temporal\Samples\NexusMultipleArguments\Service\HelloInput; -use Temporal\Samples\NexusMultipleArguments\Service\HelloOutput; -use Temporal\Samples\NexusMultipleArguments\Service\Language; -use Temporal\Samples\NexusMultipleArguments\Service\SampleNexusService; +use Temporal\Samples\Nexus\Service\HelloInput; +use Temporal\Samples\Nexus\Service\HelloOutput; +use Temporal\Samples\Nexus\Service\Language; +use Temporal\Samples\Nexus\Service\SampleNexusService; use Temporal\Workflow; use Temporal\Workflow\NexusOperationOptions; diff --git a/app/tests/Feature/bootstrap.php b/app/tests/Feature/bootstrap.php index 70f9709..62dfea9 100644 --- a/app/tests/Feature/bootstrap.php +++ b/app/tests/Feature/bootstrap.php @@ -15,9 +15,11 @@ // Nexus needs the full Temporal server; the time-skipping test server // shipped via `startTemporalTestServer()` doesn't expose the Nexus APIs. // Mirrors what sdk-php's acceptance harness does in TemporalStarter. +$httpPort = getenv('TEMPORAL_HTTP_PORT') ?: '7246'; + $environment->startTemporalServer( parameters: [ - '--http-port', '7243', + '--http-port', $httpPort, ], ); // rr's `-c` is resolved relative to its `-w` workdir, while diff --git a/app/tests/Feature/worker.php b/app/tests/Feature/worker.php index 9e8ca89..6711ecd 100644 --- a/app/tests/Feature/worker.php +++ b/app/tests/Feature/worker.php @@ -6,7 +6,9 @@ use App\Tests\Feature\Nexus\CallerWorkflowTest; use App\Tests\Feature\Nexus\CancellationTest; use App\Tests\Feature\Nexus\ContextPropagationTest; +use App\Tests\Feature\Nexus\ManualOperationStartFailureTest; use App\Tests\Feature\Nexus\ManualOperationTest; +use App\Tests\Feature\Nexus\Mock\FailingJobClient; use App\Tests\Feature\Nexus\Mock\HeaderEchoNexusServiceImpl; use App\Tests\Feature\Nexus\Mock\MockEchoClient; use App\Tests\Feature\Nexus\Mock\MockHelloHandlerWorkflowImpl; @@ -15,9 +17,9 @@ use App\Tests\Feature\Nexus\NexusServiceMockTest; use App\Tests\Feature\Nexus\Workflow\TestCancellationCallerWorkflowImpl; use App\Tests\Feature\Nexus\Workflow\TestContextEchoCallerWorkflowImpl; +use App\Tests\Feature\Nexus\Workflow\TestContextHelloCallerWorkflowImpl; use App\Tests\Feature\Nexus\Workflow\TestEchoCallerWorkflowImpl; use App\Tests\Feature\Nexus\Workflow\TestHelloCallerWorkflowImpl; -use App\Tests\Feature\Nexus\Workflow\TestHelloWithTokenCallerWorkflowImpl; use App\Tests\Feature\Nexus\Workflow\TestManualJobCallerWorkflowImpl; use App\Tests\Feature\Nexus\Workflow\TestMultiArgsHelloCallerWorkflowImpl; use Temporal\Client\GRPC\ServiceClient; @@ -26,6 +28,8 @@ use Temporal\Samples\Nexus\Handler\HelloHandlerWorkflowImpl; use Temporal\Samples\Nexus\Handler\SampleNexusServiceImpl; use Temporal\Samples\NexusContextPropagation\Propagation\NexusOutboundContextInterceptor; +use Temporal\Samples\NexusContextPropagation\Propagation\NexusStartContextInterceptor; +use Temporal\Samples\NexusContextPropagation\Propagation\WorkflowInboundContextInterceptor; use Temporal\Testing\WorkerFactory; ini_set('display_errors', 'stderr'); @@ -37,6 +41,7 @@ // the worker needs a WorkflowClient threaded through to the operation context. $workflowClient = WorkflowClient::create( ServiceClient::create(\getenv('TEMPORAL_ADDRESS') ?: 'localhost:7236'), + interceptorProvider: new SimplePipelineProvider([new NexusStartContextInterceptor()]), ); $workerFactory = WorkerFactory::create(client: $workflowClient); @@ -61,7 +66,6 @@ ->registerWorkflowTypes( TestEchoCallerWorkflowImpl::class, TestHelloCallerWorkflowImpl::class, - TestHelloWithTokenCallerWorkflowImpl::class, HelloHandlerWorkflowImpl::class, ) ->registerNexusServiceImplementation(new SampleNexusServiceImpl()); @@ -96,7 +100,7 @@ TestCancellationCallerWorkflowImpl::class, \Temporal\Samples\NexusCancellation\Handler\HelloHandlerWorkflowImpl::class, ) - ->registerNexusServiceImplementation(new \Temporal\Samples\NexusCancellation\Handler\SampleNexusServiceImpl()); + ->registerNexusServiceImplementation(new \Temporal\Samples\Nexus\Handler\SampleNexusServiceImpl()); // Scenario 5: the NexusContextPropagation sample β€” the production outbound // interceptor on the caller side, a header-echoing service double on the @@ -105,10 +109,12 @@ taskQueue: ContextPropagationTest::TASK_QUEUE, interceptorProvider: new SimplePipelineProvider([ new NexusOutboundContextInterceptor(), + new WorkflowInboundContextInterceptor(), ]), ) ->registerWorkflowTypes( TestContextEchoCallerWorkflowImpl::class, + TestContextHelloCallerWorkflowImpl::class, \Temporal\Samples\NexusContextPropagation\Handler\HelloHandlerWorkflowImpl::class, ) ->registerNexusServiceImplementation(new HeaderEchoNexusServiceImpl()); @@ -128,4 +134,12 @@ ->registerWorkflowTypes(TestManualJobCallerWorkflowImpl::class) ->registerNexusServiceImplementation(new \Temporal\Samples\NexusManualOperation\Handler\SampleNexusService()); +// Scenario 8: same manual-operation caller, but the external job backend +// rejects the async submit so the caller must surface the start failure. +$workerFactory->newWorker(taskQueue: ManualOperationStartFailureTest::TASK_QUEUE) + ->registerWorkflowTypes(TestManualJobCallerWorkflowImpl::class) + ->registerNexusServiceImplementation( + new \Temporal\Samples\NexusManualOperation\Handler\SampleNexusService(new FailingJobClient()), + ); + $workerFactory->run(); From fe203744bfaefab55a01b7b0545e007d92f3ad6d Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Wed, 19 Aug 2026 17:05:25 +0400 Subject: [PATCH 4/5] fix(tests): address the feature test server over IPv4 RoadRunner resolved localhost to ::1 while the dev server listens on 127.0.0.1, so no worker could connect. --- app/composer.lock | 8 ++++---- app/phpunit.xml.dist | 2 +- app/tests/Feature/.rr.test.yaml | 2 +- app/tests/Feature/Nexus/NexusTestCase.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/composer.lock b/app/composer.lock index 4c5a985..fe68a97 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4514,12 +4514,12 @@ "source": { "type": "git", "url": "https://github.com/temporalio/sdk-php.git", - "reference": "85126b125b82c5218c70d9b96a1d8bbdbafd00b7" + "reference": "fec3ab3c9959be0100e2a5aef87e15933a7a81a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/85126b125b82c5218c70d9b96a1d8bbdbafd00b7", - "reference": "85126b125b82c5218c70d9b96a1d8bbdbafd00b7", + "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/fec3ab3c9959be0100e2a5aef87e15933a7a81a2", + "reference": "fec3ab3c9959be0100e2a5aef87e15933a7a81a2", "shasum": "" }, "require": { @@ -4674,7 +4674,7 @@ "issues": "https://github.com/temporalio/sdk-php/issues", "source": "https://github.com/temporalio/sdk-php" }, - "time": "2026-08-18T15:44:58+00:00" + "time": "2026-08-19T09:15:08+00:00" } ], "packages-dev": [ diff --git a/app/phpunit.xml.dist b/app/phpunit.xml.dist index 497fc38..84b37f3 100644 --- a/app/phpunit.xml.dist +++ b/app/phpunit.xml.dist @@ -18,7 +18,7 @@ - + diff --git a/app/tests/Feature/.rr.test.yaml b/app/tests/Feature/.rr.test.yaml index 56afacf..1c3c316 100644 --- a/app/tests/Feature/.rr.test.yaml +++ b/app/tests/Feature/.rr.test.yaml @@ -7,7 +7,7 @@ server: command: "php worker.php" temporal: - address: ${TEMPORAL_ADDRESS:-localhost:7236} + address: ${TEMPORAL_ADDRESS:-127.0.0.1:7236} activities: num_workers: 2 diff --git a/app/tests/Feature/Nexus/NexusTestCase.php b/app/tests/Feature/Nexus/NexusTestCase.php index c928f93..2aee08c 100644 --- a/app/tests/Feature/Nexus/NexusTestCase.php +++ b/app/tests/Feature/Nexus/NexusTestCase.php @@ -32,7 +32,7 @@ public static function setUpBeforeClass(): void self::fail(\sprintf('%s must override the TASK_QUEUE constant.', static::class)); } - self::$nexusHelper = new NexusEndpointHelper(\getenv('TEMPORAL_ADDRESS') ?: 'localhost:7236'); + self::$nexusHelper = new NexusEndpointHelper(\getenv('TEMPORAL_ADDRESS') ?: '127.0.0.1:7236'); self::$sharedEndpoint = self::$nexusHelper->setupEndpoint( namespace: 'default', taskQueue: static::TASK_QUEUE, From bd797562998da729fd0994e440b2d277333ae157 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Wed, 19 Aug 2026 20:32:43 +0400 Subject: [PATCH 5/5] test(nexus): run the shipped sample workflows The feature tests kept forked copies of every caller workflow because the endpoint name is a constant inside the sample. The harness now creates the endpoint under that same name, so the samples run as they ship. --- .../Feature/Nexus/CallerWorkflowMockTest.php | 14 +-- .../Feature/Nexus/CallerWorkflowTest.php | 18 ++-- app/tests/Feature/Nexus/CancellationTest.php | 26 +++-- .../Feature/Nexus/ContextPropagationTest.php | 15 +-- .../Nexus/ManualOperationStartFailureTest.php | 8 +- .../Feature/Nexus/ManualOperationTest.php | 9 +- .../Feature/Nexus/MultipleArgumentsTest.php | 8 +- .../Feature/Nexus/NexusEndpointHelper.php | 25 ++++- .../Feature/Nexus/NexusServiceMockTest.php | 14 +-- app/tests/Feature/Nexus/NexusTestCase.php | 4 + .../TestCancellationCallerWorkflow.php | 15 --- .../TestCancellationCallerWorkflowImpl.php | 66 ------------- .../TestContextEchoCallerWorkflow.php | 15 --- .../TestContextEchoCallerWorkflowImpl.php | 33 ------- .../TestContextHelloCallerWorkflow.php | 15 --- .../TestContextHelloCallerWorkflowImpl.php | 34 ------- .../Nexus/Workflow/TestEchoCallerWorkflow.php | 20 ---- .../Workflow/TestEchoCallerWorkflowImpl.php | 29 ------ .../Workflow/TestHelloCallerWorkflow.php | 16 --- .../Workflow/TestHelloCallerWorkflowImpl.php | 30 ------ .../Workflow/TestManualJobCallerWorkflow.php | 15 --- .../TestManualJobCallerWorkflowImpl.php | 98 ------------------- .../TestMultiArgsHelloCallerWorkflow.php | 16 --- .../TestMultiArgsHelloCallerWorkflowImpl.php | 31 ------ app/tests/Feature/worker.php | 31 +++--- 25 files changed, 110 insertions(+), 495 deletions(-) delete mode 100644 app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflow.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflowImpl.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflow.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflowImpl.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflow.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflowImpl.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflow.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflowImpl.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflow.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflowImpl.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflow.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflowImpl.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php delete mode 100644 app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflowImpl.php diff --git a/app/tests/Feature/Nexus/CallerWorkflowMockTest.php b/app/tests/Feature/Nexus/CallerWorkflowMockTest.php index 9ce1125..2a00b5b 100644 --- a/app/tests/Feature/Nexus/CallerWorkflowMockTest.php +++ b/app/tests/Feature/Nexus/CallerWorkflowMockTest.php @@ -4,10 +4,11 @@ namespace App\Tests\Feature\Nexus; +use Temporal\Samples\Nexus\Caller\HelloCallerWorkflow; +use Temporal\Samples\Nexus\Caller\EchoCallerWorkflow; +use Temporal\Samples\Nexus\Caller\CallerWorker; use App\Tests\Feature\Nexus\Mock\MockEchoClient; use App\Tests\Feature\Nexus\Mock\MockHelloHandlerWorkflowImpl; -use App\Tests\Feature\Nexus\Workflow\TestEchoCallerWorkflow; -use App\Tests\Feature\Nexus\Workflow\TestHelloCallerWorkflow; use Temporal\Samples\Nexus\Service\Language; /** @@ -18,21 +19,22 @@ final class CallerWorkflowMockTest extends NexusTestCase { public const TASK_QUEUE = 'nexus-test-mock-handler'; + public const ENDPOINT_NAME = CallerWorker::ENDPOINT_NAME; public function testEchoUsesMockedClient(): void { - $workflow = $this->newCaller(TestEchoCallerWorkflow::class); + $workflow = $this->newCaller(EchoCallerWorkflow::class); - $result = $workflow->echo($this->endpoint['name'], 'ignored'); + $result = $workflow->echo('ignored'); self::assertSame(MockEchoClient::CANNED, $result); } public function testHelloUsesMockHandlerWorkflow(): void { - $workflow = $this->newCaller(TestHelloCallerWorkflow::class); + $workflow = $this->newCaller(HelloCallerWorkflow::class); - $result = $workflow->hello($this->endpoint['name'], 'World', Language::EN); + $result = $workflow->hello('World', Language::EN); self::assertSame(MockHelloHandlerWorkflowImpl::CANNED, $result); } diff --git a/app/tests/Feature/Nexus/CallerWorkflowTest.php b/app/tests/Feature/Nexus/CallerWorkflowTest.php index 1006d71..b47cf59 100644 --- a/app/tests/Feature/Nexus/CallerWorkflowTest.php +++ b/app/tests/Feature/Nexus/CallerWorkflowTest.php @@ -4,8 +4,9 @@ namespace App\Tests\Feature\Nexus; -use App\Tests\Feature\Nexus\Workflow\TestEchoCallerWorkflow; -use App\Tests\Feature\Nexus\Workflow\TestHelloCallerWorkflow; +use Temporal\Samples\Nexus\Caller\HelloCallerWorkflow; +use Temporal\Samples\Nexus\Caller\EchoCallerWorkflow; +use Temporal\Samples\Nexus\Caller\CallerWorker; use Temporal\Samples\Nexus\Service\Language; /** @@ -17,30 +18,31 @@ final class CallerWorkflowTest extends NexusTestCase { public const TASK_QUEUE = 'nexus-test-real'; + public const ENDPOINT_NAME = CallerWorker::ENDPOINT_NAME; public function testEchoRoundTrip(): void { - $workflow = $this->newCaller(TestEchoCallerWorkflow::class); + $workflow = $this->newCaller(EchoCallerWorkflow::class); - $result = $workflow->echo($this->endpoint['name'], 'Hello'); + $result = $workflow->echo('Hello'); self::assertSame('Hello', $result); } public function testHelloRoundTrip(): void { - $workflow = $this->newCaller(TestHelloCallerWorkflow::class); + $workflow = $this->newCaller(HelloCallerWorkflow::class); - $result = $workflow->hello($this->endpoint['name'], 'World', Language::EN); + $result = $workflow->hello('World', Language::EN); self::assertSame('Hello World πŸ‘‹', $result); } public function testHelloRespectsLanguage(): void { - $workflow = $this->newCaller(TestHelloCallerWorkflow::class); + $workflow = $this->newCaller(HelloCallerWorkflow::class); - $result = $workflow->hello($this->endpoint['name'], 'Nexus', Language::ES); + $result = $workflow->hello('Nexus', Language::ES); self::assertSame('Β‘Hola! Nexus πŸ‘‹', $result); } diff --git a/app/tests/Feature/Nexus/CancellationTest.php b/app/tests/Feature/Nexus/CancellationTest.php index c9ac5cf..392abbe 100644 --- a/app/tests/Feature/Nexus/CancellationTest.php +++ b/app/tests/Feature/Nexus/CancellationTest.php @@ -4,7 +4,9 @@ namespace App\Tests\Feature\Nexus; -use App\Tests\Feature\Nexus\Workflow\TestCancellationCallerWorkflow; +use Temporal\Api\History\V1\HistoryEvent; +use Temporal\Samples\NexusCancellation\Caller\CallerWorker; +use Temporal\Samples\NexusCancellation\Caller\HelloCallerWorkflow; /** * End-to-end run of the NexusCancellation sample flow: fan out the same @@ -13,20 +15,32 @@ final class CancellationTest extends NexusTestCase { public const TASK_QUEUE = 'nexus-test-cancellation'; + public const ENDPOINT_NAME = CallerWorker::ENDPOINT_NAME; public function testFirstReplyWinsAndOthersAreCancelled(): void { - $workflow = $this->newCaller(TestCancellationCallerWorkflow::class); + $workflow = $this->newCaller(HelloCallerWorkflow::class); + $run = $this->workflowClient->start($workflow, 'Nexus'); - $result = $workflow->hello($this->endpoint['name'], 'Nexus'); - - self::assertStringEndsWith(' [cancelled=4]', $result); - self::assertContains(\substr($result, 0, -\strlen(' [cancelled=4]')), [ + self::assertContains($run->getResult('string'), [ 'Hello Nexus πŸ‘‹', 'Bonjour Nexus πŸ‘‹', 'Hallo Nexus πŸ‘‹', 'Β‘Hola! Nexus πŸ‘‹', 'Merhaba Nexus πŸ‘‹', ]); + + self::assertSame(4, $this->countCancelRequests($run->getExecution())); + } + + private function countCancelRequests(\Temporal\Workflow\WorkflowExecution $execution): int + { + $count = 0; + foreach ($this->workflowClient->getWorkflowHistory($execution) as $event) { + \assert($event instanceof HistoryEvent); + $event->hasNexusOperationCancelRequestedEventAttributes() and ++$count; + } + + return $count; } } diff --git a/app/tests/Feature/Nexus/ContextPropagationTest.php b/app/tests/Feature/Nexus/ContextPropagationTest.php index 94366b1..99cf4b7 100644 --- a/app/tests/Feature/Nexus/ContextPropagationTest.php +++ b/app/tests/Feature/Nexus/ContextPropagationTest.php @@ -4,8 +4,10 @@ namespace App\Tests\Feature\Nexus; -use App\Tests\Feature\Nexus\Workflow\TestContextEchoCallerWorkflow; -use App\Tests\Feature\Nexus\Workflow\TestContextHelloCallerWorkflow; +use Temporal\Samples\NexusContextPropagation\Caller\CallerWorker; +use Temporal\Samples\Nexus\Caller\EchoCallerWorkflow; +use Temporal\Samples\Nexus\Service\Language; +use Temporal\Samples\Nexus\Caller\HelloCallerWorkflow; use Temporal\Client\WorkflowOptions; /** @@ -17,16 +19,17 @@ final class ContextPropagationTest extends NexusTestCase { public const TASK_QUEUE = 'nexus-test-context'; + public const ENDPOINT_NAME = CallerWorker::ENDPOINT_NAME; public function testCallerWorkflowIdPropagatesViaHeaders(): void { $workflowId = 'ctx-prop-' . \bin2hex(\random_bytes(4)); $workflow = $this->newCaller( - TestContextEchoCallerWorkflow::class, + EchoCallerWorkflow::class, WorkflowOptions::new()->withWorkflowId($workflowId), ); - $result = $workflow->echo($this->endpoint['name'], 'ignored'); + $result = $workflow->echo('ignored'); self::assertSame($workflowId, $result); } @@ -35,11 +38,11 @@ public function testCallerWorkflowIdReachesTheBackingWorkflow(): void { $workflowId = 'ctx-prop-async-' . \bin2hex(\random_bytes(4)); $workflow = $this->newCaller( - TestContextHelloCallerWorkflow::class, + HelloCallerWorkflow::class, WorkflowOptions::new()->withWorkflowId($workflowId), ); - $result = $workflow->hello($this->endpoint['name'], 'Nexus'); + $result = $workflow->hello('Nexus', Language::EN); self::assertStringContainsString($workflowId, $result); } diff --git a/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php b/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php index 138a189..c672447 100644 --- a/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php +++ b/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php @@ -5,8 +5,9 @@ namespace App\Tests\Feature\Nexus; use App\Tests\Feature\Nexus\Mock\FailingJobClient; -use App\Tests\Feature\Nexus\Workflow\TestManualJobCallerWorkflow; use Carbon\CarbonInterval; +use Temporal\Samples\NexusManualOperation\Caller\CallerWorker; +use Temporal\Samples\NexusManualOperation\Caller\JobCallerWorkflow; use Temporal\Client\WorkflowOptions; use Temporal\Exception\Client\WorkflowFailedException; @@ -17,16 +18,17 @@ final class ManualOperationStartFailureTest extends NexusTestCase { public const TASK_QUEUE = 'nexus-test-manual-start-failure'; + public const ENDPOINT_NAME = CallerWorker::ENDPOINT_NAME; public function testAsyncStartFailureFailsTheWorkflow(): void { $workflow = $this->newCaller( - TestManualJobCallerWorkflow::class, + JobCallerWorkflow::class, WorkflowOptions::new()->withWorkflowExecutionTimeout(CarbonInterval::seconds(15)), ); try { - $workflow->run($this->endpoint['name'], 'Demo'); + $workflow->run('Demo'); self::fail('Expected the workflow to fail with the handler error.'); } catch (WorkflowFailedException $e) { self::assertStringContainsString( diff --git a/app/tests/Feature/Nexus/ManualOperationTest.php b/app/tests/Feature/Nexus/ManualOperationTest.php index 65ab7db..adfc0fa 100644 --- a/app/tests/Feature/Nexus/ManualOperationTest.php +++ b/app/tests/Feature/Nexus/ManualOperationTest.php @@ -4,7 +4,9 @@ namespace App\Tests\Feature\Nexus; -use App\Tests\Feature\Nexus\Workflow\TestManualJobCallerWorkflow; +use Temporal\Samples\NexusManualOperation\Caller\JobCallerWorkflow; +use Temporal\Samples\NexusManualOperation\Caller\CallerWorker; + /** * End-to-end run of the NexusManualOperation sample flow: a manual @@ -14,12 +16,13 @@ final class ManualOperationTest extends NexusTestCase { public const TASK_QUEUE = 'nexus-test-manual'; + public const ENDPOINT_NAME = CallerWorker::ENDPOINT_NAME; public function testSyncFastPathTokenAndCancel(): void { - $workflow = $this->newCaller(TestManualJobCallerWorkflow::class); + $workflow = $this->newCaller(JobCallerWorkflow::class); - $result = $workflow->run($this->endpoint['name'], 'Demo'); + $result = $workflow->run('Demo'); self::assertStringContainsString('done instantly: Demo', $result); self::assertStringContainsString('[token=job-', $result); diff --git a/app/tests/Feature/Nexus/MultipleArgumentsTest.php b/app/tests/Feature/Nexus/MultipleArgumentsTest.php index 3e8669e..2391f69 100644 --- a/app/tests/Feature/Nexus/MultipleArgumentsTest.php +++ b/app/tests/Feature/Nexus/MultipleArgumentsTest.php @@ -4,7 +4,8 @@ namespace App\Tests\Feature\Nexus; -use App\Tests\Feature\Nexus\Workflow\TestMultiArgsHelloCallerWorkflow; +use Temporal\Samples\NexusMultipleArguments\Caller\HelloCallerWorkflow; +use Temporal\Samples\NexusMultipleArguments\Caller\CallerWorker; use Temporal\Samples\Nexus\Service\Language; /** @@ -14,12 +15,13 @@ final class MultipleArgumentsTest extends NexusTestCase { public const TASK_QUEUE = 'nexus-test-multiargs'; + public const ENDPOINT_NAME = CallerWorker::ENDPOINT_NAME; public function testHelloUnpacksDtoIntoWorkflowArguments(): void { - $workflow = $this->newCaller(TestMultiArgsHelloCallerWorkflow::class); + $workflow = $this->newCaller(HelloCallerWorkflow::class); - $result = $workflow->hello($this->endpoint['name'], 'World', Language::ES); + $result = $workflow->hello('World', Language::ES); self::assertSame('Β‘Hola! World πŸ‘‹', $result); } diff --git a/app/tests/Feature/Nexus/NexusEndpointHelper.php b/app/tests/Feature/Nexus/NexusEndpointHelper.php index 95f1c30..d9e533c 100644 --- a/app/tests/Feature/Nexus/NexusEndpointHelper.php +++ b/app/tests/Feature/Nexus/NexusEndpointHelper.php @@ -10,6 +10,7 @@ use Temporal\Api\Operatorservice\V1\CreateNexusEndpointRequest; use Temporal\Api\Operatorservice\V1\DeleteNexusEndpointRequest; use Temporal\Api\Operatorservice\V1\GetNexusEndpointRequest; +use Temporal\Api\Operatorservice\V1\ListNexusEndpointsRequest; use Temporal\Api\Operatorservice\V1\OperatorServiceClient; /** @@ -36,9 +37,10 @@ public function __construct(string $temporalAddress) * * @return array{id: string, name: string} */ - public function setupEndpoint(string $namespace, string $taskQueue, string $prefix = 'samples-test'): array + public function setupEndpoint(string $namespace, string $taskQueue, ?string $name = null): array { - $name = $prefix . '-' . \bin2hex(\random_bytes(4)); + $name ??= 'samples-test-' . \bin2hex(\random_bytes(4)); + $this->deleteEndpointByName($name); $request = (new CreateNexusEndpointRequest()) ->setSpec( @@ -92,6 +94,25 @@ public function close(): void $this->operator->close(); } + /** + * Endpoint names are global, and samples pin theirs by constant, so a + * previous run that died mid-test can leave one behind. + */ + private function deleteEndpointByName(string $name): void + { + [$response, $status] = $this->operator + ->ListNexusEndpoints((new ListNexusEndpointsRequest())->setName($name)) + ->wait(); + + if ($status->code !== \Grpc\STATUS_OK) { + return; + } + + foreach ($response->getEndpoints() as $endpoint) { + $this->deleteEndpoint($endpoint->getId(), $endpoint->getVersion()); + } + } + public function deleteEndpoint(string $endpointId, int $expectedVersion = 1): void { $request = (new DeleteNexusEndpointRequest()) diff --git a/app/tests/Feature/Nexus/NexusServiceMockTest.php b/app/tests/Feature/Nexus/NexusServiceMockTest.php index 2888c21..a5bc621 100644 --- a/app/tests/Feature/Nexus/NexusServiceMockTest.php +++ b/app/tests/Feature/Nexus/NexusServiceMockTest.php @@ -4,10 +4,11 @@ namespace App\Tests\Feature\Nexus; +use Temporal\Samples\Nexus\Caller\HelloCallerWorkflow; +use Temporal\Samples\Nexus\Caller\EchoCallerWorkflow; +use Temporal\Samples\Nexus\Caller\CallerWorker; use App\Tests\Feature\Nexus\Mock\MockHelloHandlerWorkflowImpl; use App\Tests\Feature\Nexus\Mock\MockSampleNexusServiceImpl; -use App\Tests\Feature\Nexus\Workflow\TestEchoCallerWorkflow; -use App\Tests\Feature\Nexus\Workflow\TestHelloCallerWorkflow; use Temporal\Samples\Nexus\Service\Language; /** @@ -17,21 +18,22 @@ final class NexusServiceMockTest extends NexusTestCase { public const TASK_QUEUE = 'nexus-test-mock-service'; + public const ENDPOINT_NAME = CallerWorker::ENDPOINT_NAME; public function testEchoFromMockedService(): void { - $workflow = $this->newCaller(TestEchoCallerWorkflow::class); + $workflow = $this->newCaller(EchoCallerWorkflow::class); - $result = $workflow->echo($this->endpoint['name'], 'ignored'); + $result = $workflow->echo('ignored'); self::assertSame(MockSampleNexusServiceImpl::ECHO_CANNED, $result); } public function testHelloFromMockedService(): void { - $workflow = $this->newCaller(TestHelloCallerWorkflow::class); + $workflow = $this->newCaller(HelloCallerWorkflow::class); - $result = $workflow->hello($this->endpoint['name'], 'World', Language::DE); + $result = $workflow->hello('World', Language::DE); self::assertSame(MockHelloHandlerWorkflowImpl::CANNED, $result); } diff --git a/app/tests/Feature/Nexus/NexusTestCase.php b/app/tests/Feature/Nexus/NexusTestCase.php index 2aee08c..1ced641 100644 --- a/app/tests/Feature/Nexus/NexusTestCase.php +++ b/app/tests/Feature/Nexus/NexusTestCase.php @@ -16,6 +16,9 @@ abstract class NexusTestCase extends TestCase { public const TASK_QUEUE = ''; + /** Endpoint name the sample under test has baked into its caller. */ + public const ENDPOINT_NAME = ''; + private static ?NexusEndpointHelper $nexusHelper = null; /** @var array{id: string, name: string} */ @@ -36,6 +39,7 @@ public static function setUpBeforeClass(): void self::$sharedEndpoint = self::$nexusHelper->setupEndpoint( namespace: 'default', taskQueue: static::TASK_QUEUE, + name: static::ENDPOINT_NAME === '' ? null : static::ENDPOINT_NAME, ); } diff --git a/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflow.php deleted file mode 100644 index 0b98c46..0000000 --- a/app/tests/Feature/Nexus/Workflow/TestCancellationCallerWorkflow.php +++ /dev/null @@ -1,15 +0,0 @@ -withEndpoint($endpoint) - ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)) - ->withCancellationType(NexusOperationCancellationType::WaitRequested), - ); - - $promises = []; - $scope = Workflow::async(function () use ($service, $message, &$promises): void { - foreach (Language::cases() as $language) { - $promises[] = $service->hello(new HelloInput($message, $language)); - } - }); - - /** @var HelloOutput $first */ - $first = yield Promise::any($promises); - - $scope->cancel(); - - $cancelled = 0; - - foreach ($promises as $promise) { - try { - yield $promise; - } catch (CanceledFailure) { - ++$cancelled; - } catch (NexusOperationFailure $e) { - if (!$e->getPrevious() instanceof CanceledFailure) { - throw $e; - } - - ++$cancelled; - } - } - - return "{$first->message} [cancelled={$cancelled}]"; - } -} diff --git a/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflow.php deleted file mode 100644 index 6c09331..0000000 --- a/app/tests/Feature/Nexus/Workflow/TestContextEchoCallerWorkflow.php +++ /dev/null @@ -1,15 +0,0 @@ -execution->getID()); - - /** @var SampleNexusService $service */ - $service = Workflow::newNexusServiceStub( - SampleNexusService::class, - NexusOperationOptions::new() - ->withEndpoint($endpoint) - ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), - ); - - /** @var EchoOutput $output */ - $output = yield $service->echo(new EchoInput($message)); - return $output->message; - } -} diff --git a/app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflow.php deleted file mode 100644 index 95f7034..0000000 --- a/app/tests/Feature/Nexus/Workflow/TestContextHelloCallerWorkflow.php +++ /dev/null @@ -1,15 +0,0 @@ -execution->getID()); - - /** @var SampleNexusService $service */ - $service = Workflow::newNexusServiceStub( - SampleNexusService::class, - NexusOperationOptions::new() - ->withEndpoint($endpoint) - ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), - ); - - /** @var HelloOutput $output */ - $output = yield $service->hello(new HelloInput($name, Language::EN)); - return $output->message; - } -} diff --git a/app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflow.php deleted file mode 100644 index 1c15f1e..0000000 --- a/app/tests/Feature/Nexus/Workflow/TestEchoCallerWorkflow.php +++ /dev/null @@ -1,20 +0,0 @@ -withEndpoint($endpoint) - ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), - ); - /** @var EchoOutput $output */ - $output = yield $stub->echo(new EchoInput($message)); - return $output->message; - } -} diff --git a/app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflow.php deleted file mode 100644 index 40775b2..0000000 --- a/app/tests/Feature/Nexus/Workflow/TestHelloCallerWorkflow.php +++ /dev/null @@ -1,16 +0,0 @@ -withEndpoint($endpoint) - ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), - ); - /** @var HelloOutput $output */ - $output = yield $stub->hello(new HelloInput($name, $language)); - return $output->message; - } -} diff --git a/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflow.php deleted file mode 100644 index 94ba12a..0000000 --- a/app/tests/Feature/Nexus/Workflow/TestManualJobCallerWorkflow.php +++ /dev/null @@ -1,15 +0,0 @@ -withEndpoint($endpoint) - ->withService('SampleNexusService') - ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)) - ->withCancellationType(NexusOperationCancellationType::TryCancel), - ); - - /** @var NexusOperationHandle $handle */ - $handle = yield $stub->start( - 'startJob', - [new JobInput($jobName, instant: true)], - JobResult::class, - ); - - if ($handle->getOperationToken() !== null) { - throw new \LogicException('Sync result must not carry an operation token.'); - } - - /** @var JobResult $instantResult */ - $instantResult = yield $handle->getResult(); - - $handle2 = null; - $failure = null; - $scope = Workflow::async(static function () use ($stub, $jobName, &$handle2, &$failure): \Generator { - try { - $handle2 = yield $stub->start( - 'startJob', - [new JobInput($jobName)], - JobResult::class, - ); - } catch (\Throwable $e) { - $failure = $e; - throw $e; - } - - yield $handle2->getResult(); - }); - - yield Workflow::await(function () use (&$handle2, &$failure): bool { - return $handle2 !== null || $failure !== null; - }); - - if ($failure !== null) { - throw $failure; - } - - $token2 = $handle2->getOperationToken(); - - yield Workflow::timer(1); - $scope->cancel(); - - $cancelled = false; - - try { - yield $scope; - } catch (CanceledFailure) { - $cancelled = true; - } catch (NexusOperationFailure $e) { - if (!$e->getPrevious() instanceof CanceledFailure) { - throw $e; - } - - $cancelled = true; - } - - if (!$cancelled) { - throw new \LogicException('Async operation did not end cancelled.'); - } - - return "[instant={$instantResult->message}] [token={$token2}] cancelled"; - } -} diff --git a/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php b/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php deleted file mode 100644 index 5ad476d..0000000 --- a/app/tests/Feature/Nexus/Workflow/TestMultiArgsHelloCallerWorkflow.php +++ /dev/null @@ -1,16 +0,0 @@ -withEndpoint($endpoint) - ->withScheduleToCloseTimeout(CarbonInterval::seconds(20)), - ); - - /** @var HelloOutput $output */ - $output = yield $service->hello(new HelloInput($name, $language)); - return $output->message; - } -} diff --git a/app/tests/Feature/worker.php b/app/tests/Feature/worker.php index 6711ecd..602a019 100644 --- a/app/tests/Feature/worker.php +++ b/app/tests/Feature/worker.php @@ -15,13 +15,6 @@ use App\Tests\Feature\Nexus\Mock\MockSampleNexusServiceImpl; use App\Tests\Feature\Nexus\MultipleArgumentsTest; use App\Tests\Feature\Nexus\NexusServiceMockTest; -use App\Tests\Feature\Nexus\Workflow\TestCancellationCallerWorkflowImpl; -use App\Tests\Feature\Nexus\Workflow\TestContextEchoCallerWorkflowImpl; -use App\Tests\Feature\Nexus\Workflow\TestContextHelloCallerWorkflowImpl; -use App\Tests\Feature\Nexus\Workflow\TestEchoCallerWorkflowImpl; -use App\Tests\Feature\Nexus\Workflow\TestHelloCallerWorkflowImpl; -use App\Tests\Feature\Nexus\Workflow\TestManualJobCallerWorkflowImpl; -use App\Tests\Feature\Nexus\Workflow\TestMultiArgsHelloCallerWorkflowImpl; use Temporal\Client\GRPC\ServiceClient; use Temporal\Client\WorkflowClient; use Temporal\Interceptor\SimplePipelineProvider; @@ -64,8 +57,8 @@ // Scenario 1: real production handler. Mirrors `php app/app.php nexus`. $workerFactory->newWorker(taskQueue: CallerWorkflowTest::TASK_QUEUE) ->registerWorkflowTypes( - TestEchoCallerWorkflowImpl::class, - TestHelloCallerWorkflowImpl::class, + \Temporal\Samples\Nexus\Caller\EchoCallerWorkflowImpl::class, + \Temporal\Samples\Nexus\Caller\HelloCallerWorkflowImpl::class, HelloHandlerWorkflowImpl::class, ) ->registerNexusServiceImplementation(new SampleNexusServiceImpl()); @@ -75,8 +68,8 @@ // the DI-mock + workflow-replacement test style. $workerFactory->newWorker(taskQueue: CallerWorkflowMockTest::TASK_QUEUE) ->registerWorkflowTypes( - TestEchoCallerWorkflowImpl::class, - TestHelloCallerWorkflowImpl::class, + \Temporal\Samples\Nexus\Caller\EchoCallerWorkflowImpl::class, + \Temporal\Samples\Nexus\Caller\HelloCallerWorkflowImpl::class, MockHelloHandlerWorkflowImpl::class, ) ->registerNexusServiceImplementation(new SampleNexusServiceImpl(new MockEchoClient())); @@ -87,8 +80,8 @@ // workflow start so the wire-level state machine is exercised end-to-end. $workerFactory->newWorker(taskQueue: NexusServiceMockTest::TASK_QUEUE) ->registerWorkflowTypes( - TestEchoCallerWorkflowImpl::class, - TestHelloCallerWorkflowImpl::class, + \Temporal\Samples\Nexus\Caller\EchoCallerWorkflowImpl::class, + \Temporal\Samples\Nexus\Caller\HelloCallerWorkflowImpl::class, MockHelloHandlerWorkflowImpl::class, ) ->registerNexusServiceImplementation(new MockSampleNexusServiceImpl()); @@ -97,7 +90,7 @@ // workflow that sleeps and honours cancellation. $workerFactory->newWorker(taskQueue: CancellationTest::TASK_QUEUE) ->registerWorkflowTypes( - TestCancellationCallerWorkflowImpl::class, + \Temporal\Samples\NexusCancellation\Caller\HelloCallerWorkflowImpl::class, \Temporal\Samples\NexusCancellation\Handler\HelloHandlerWorkflowImpl::class, ) ->registerNexusServiceImplementation(new \Temporal\Samples\Nexus\Handler\SampleNexusServiceImpl()); @@ -113,8 +106,8 @@ ]), ) ->registerWorkflowTypes( - TestContextEchoCallerWorkflowImpl::class, - TestContextHelloCallerWorkflowImpl::class, + \Temporal\Samples\NexusContextPropagation\Caller\EchoCallerWorkflowImpl::class, + \Temporal\Samples\NexusContextPropagation\Caller\HelloCallerWorkflowImpl::class, \Temporal\Samples\NexusContextPropagation\Handler\HelloHandlerWorkflowImpl::class, ) ->registerNexusServiceImplementation(new HeaderEchoNexusServiceImpl()); @@ -123,7 +116,7 @@ // by a multi-argument handler workflow. $workerFactory->newWorker(taskQueue: MultipleArgumentsTest::TASK_QUEUE) ->registerWorkflowTypes( - TestMultiArgsHelloCallerWorkflowImpl::class, + \Temporal\Samples\NexusMultipleArguments\Caller\HelloCallerWorkflowImpl::class, \Temporal\Samples\NexusMultipleArguments\Handler\HelloHandlerWorkflowImpl::class, ) ->registerNexusServiceImplementation(new \Temporal\Samples\NexusMultipleArguments\Handler\SampleNexusServiceImpl()); @@ -131,13 +124,13 @@ // Scenario 7: the NexusManualOperation sample β€” a manual handler object that // owns start (sync fast-path or async with its own token) and cancel. $workerFactory->newWorker(taskQueue: ManualOperationTest::TASK_QUEUE) - ->registerWorkflowTypes(TestManualJobCallerWorkflowImpl::class) + ->registerWorkflowTypes(\Temporal\Samples\NexusManualOperation\Caller\JobCallerWorkflowImpl::class) ->registerNexusServiceImplementation(new \Temporal\Samples\NexusManualOperation\Handler\SampleNexusService()); // Scenario 8: same manual-operation caller, but the external job backend // rejects the async submit so the caller must surface the start failure. $workerFactory->newWorker(taskQueue: ManualOperationStartFailureTest::TASK_QUEUE) - ->registerWorkflowTypes(TestManualJobCallerWorkflowImpl::class) + ->registerWorkflowTypes(\Temporal\Samples\NexusManualOperation\Caller\JobCallerWorkflowImpl::class) ->registerNexusServiceImplementation( new \Temporal\Samples\NexusManualOperation\Handler\SampleNexusService(new FailingJobClient()), );