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.json b/app/composer.json
index fc9b985..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.17",
+ "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 5da7a68..fdcf437 100644
--- a/app/composer.lock
+++ b/app/composer.lock
@@ -4,28 +4,28 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "e6e2a963ef95741e5a7bfe40d9478877",
+ "content-hash": "bb34ba5229d49452bfefff2f47439cc5",
"packages": [
{
"name": "brick/math",
- "version": "0.14.1",
+ "version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
- "reference": "f05858549e5f9d7bb45875a75583240a38a281d0"
+ "reference": "2effe05d2177c451b86c6a073196a4034c02f211"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0",
- "reference": "f05858549e5f9d7bb45875a75583240a38a281d0",
+ "url": "https://api.github.com/repos/brick/math/zipball/2effe05d2177c451b86c6a073196a4034c02f211",
+ "reference": "2effe05d2177c451b86c6a073196a4034c02f211",
"shasum": ""
},
"require": {
"php": "^8.2"
},
"require-dev": {
- "php-coveralls/php-coveralls": "^2.2",
- "phpstan/phpstan": "2.1.22",
+ "phpstan/phpstan": "2.2.13",
+ "phpstan/phpstan-phpunit": "2.0.18",
"phpunit/phpunit": "^11.5"
},
"type": "library",
@@ -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/1.0.0"
},
"funding": [
{
@@ -64,20 +64,20 @@
"type": "github"
}
],
- "time": "2025-11-24T14:40:29+00:00"
+ "time": "2026-09-12T10:28:18+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
- "version": "3.2.0",
+ "version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon-doctrine-types.git",
- "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d"
+ "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
- "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc",
+ "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc",
"shasum": ""
},
"require": {
@@ -92,6 +92,11 @@
"phpunit/phpunit": "^10.3"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
"autoload": {
"psr-4": {
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
@@ -117,7 +122,7 @@
],
"support": {
"issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues",
- "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0"
+ "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.1"
},
"funding": [
{
@@ -133,20 +138,20 @@
"type": "tidelift"
}
],
- "time": "2024-02-09T16:56:22+00:00"
+ "time": "2026-09-06T14:11:38+00:00"
},
{
"name": "composer/semver",
- "version": "3.4.4",
+ "version": "3.5.1",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
- "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
+ "reference": "528948210cc46f967a4b98e83b19e76f2b504993"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
- "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "url": "https://api.github.com/repos/composer/semver/zipball/528948210cc46f967a4b98e83b19e76f2b504993",
+ "reference": "528948210cc46f967a4b98e83b19e76f2b504993",
"shasum": ""
},
"require": {
@@ -188,7 +193,7 @@
"homepage": "http://robbast.nl"
}
],
- "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "description": "Version comparison library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
@@ -198,7 +203,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
- "source": "https://github.com/composer/semver/tree/3.4.4"
+ "source": "https://github.com/composer/semver/tree/3.5.1"
},
"funding": [
{
@@ -210,25 +215,25 @@
"type": "github"
}
],
- "time": "2025-08-20T19:15:30+00:00"
+ "time": "2026-10-05T13:12:12+00:00"
},
{
"name": "google/common-protos",
- "version": "4.12.4",
+ "version": "4.14.3",
"source": {
"type": "git",
"url": "https://github.com/googleapis/common-protos-php.git",
- "reference": "0127156899af0df2681bd42024c60bd5360d64e3"
+ "reference": "3c219d28f513a8997ac5d098f43943e3c5b4509a"
},
"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/3c219d28f513a8997ac5d098f43943e3c5b4509a",
+ "reference": "3c219d28f513a8997ac5d098f43943e3c5b4509a",
"shasum": ""
},
"require": {
- "google/protobuf": "^4.31",
- "php": "^8.1"
+ "google/protobuf": "^4.31||^5.0",
+ "php": "^8.2"
},
"require-dev": {
"phpunit/phpunit": "^9.6"
@@ -267,9 +272,9 @@
"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.3"
},
- "time": "2025-09-20T01:29:44+00:00"
+ "time": "2026-09-14T18:27:51+00:00"
},
{
"name": "google/protobuf",
@@ -317,20 +322,20 @@
},
{
"name": "grpc/grpc",
- "version": "1.74.0",
+ "version": "1.82.0",
"source": {
"type": "git",
"url": "https://github.com/grpc/grpc-php.git",
- "reference": "32bf4dba256d60d395582fb6e4e8d3936bcdb713"
+ "reference": "be984cb608f21e96453b3cfe54c748cc7b192250"
},
"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/be984cb608f21e96453b3cfe54c748cc7b192250",
+ "reference": "be984cb608f21e96453b3cfe54c748cc7b192250",
"shasum": ""
},
"require": {
- "php": ">=7.0.0"
+ "php": ">=7.1.0"
},
"require-dev": {
"google/auth": "^v1.3.0"
@@ -355,9 +360,9 @@
"rpc"
],
"support": {
- "source": "https://github.com/grpc/grpc-php/tree/v1.74.0"
+ "source": "https://github.com/grpc/grpc-php/tree/v1.82.0"
},
- "time": "2025-07-24T20:02:16+00:00"
+ "time": "2026-07-03T09:39:53+00:00"
},
{
"name": "internal/destroy",
@@ -493,16 +498,16 @@
},
{
"name": "nesbot/carbon",
- "version": "3.11.0",
+ "version": "3.14.2",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
- "reference": "bdb375400dcd162624531666db4799b36b64e4a1"
+ "reference": "94cfc34887f07ab5c0efa07914c8f527544e3224"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1",
- "reference": "bdb375400dcd162624531666db4799b36b64e4a1",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/94cfc34887f07ab5c0efa07914c8f527544e3224",
+ "reference": "94cfc34887f07ab5c0efa07914c8f527544e3224",
"shasum": ""
},
"require": {
@@ -522,11 +527,11 @@
"doctrine/orm": "^2.15.2 || ^3.0",
"friendsofphp/php-cs-fixer": "^v3.87.1",
"kylekatarnls/multi-tester": "^2.5.3",
- "phpmd/phpmd": "^2.15.0",
+ "phpmd/phpmd": "^3.0@dev",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.22",
"phpunit/phpunit": "^10.5.53",
- "squizlabs/php_codesniffer": "^3.13.4"
+ "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0"
},
"bin": [
"bin/carbon"
@@ -569,14 +574,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 +599,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-02T21:04:28+00:00"
+ "time": "2026-10-02T07:26:52+00:00"
},
{
"name": "nyholm/psr7-server",
@@ -1752,20 +1757,20 @@
},
{
"name": "ramsey/uuid",
- "version": "4.9.2",
+ "version": "4.9.4",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
- "reference": "8429c78ca35a09f27565311b98101e2826affde0"
+ "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0",
- "reference": "8429c78ca35a09f27565311b98101e2826affde0",
+ "url": "https://api.github.com/repos/ramsey/uuid/zipball/75d73f48d02797c2c285a7e9f348fadc0102ffe2",
+ "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2",
"shasum": ""
},
"require": {
- "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
+ "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -1824,26 +1829,26 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
- "source": "https://github.com/ramsey/uuid/tree/4.9.2"
+ "source": "https://github.com/ramsey/uuid/tree/4.9.4"
},
- "time": "2025-12-14T04:43:48+00:00"
+ "time": "2026-09-16T11:39:30+00:00"
},
{
"name": "roadrunner-php/roadrunner-api-dto",
- "version": "v1.14.0",
+ "version": "v1.18.0",
"source": {
"type": "git",
"url": "https://github.com/roadrunner-php/roadrunner-api-dto.git",
- "reference": "e6efb759f0a73b8516b7f28317230ecd4010005e"
+ "reference": "81d8247894cd3f75a4e0fba2b619f9d7efa03d27"
},
"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/81d8247894cd3f75a4e0fba2b619f9d7efa03d27",
+ "reference": "81d8247894cd3f75a4e0fba2b619f9d7efa03d27",
"shasum": ""
},
"require": {
- "google/protobuf": "^4.31.1",
+ "google/protobuf": "^4.31.1||^5.34.0",
"php": "^8.1"
},
"conflict": {
@@ -1885,7 +1890,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.18.0"
},
"funding": [
{
@@ -1893,32 +1898,34 @@
"type": "github"
}
],
- "time": "2025-11-06T13:03:11+00:00"
+ "time": "2026-10-09T15:32:54+00:00"
},
{
"name": "roadrunner-php/version-checker",
- "version": "v1.0.2",
+ "version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/roadrunner-php/version-checker.git",
- "reference": "a7994f700586265a54a2989b97f7d7f25ed5890b"
+ "reference": "028b84186c17aab4bbb32fd07976b9367aee837c"
},
"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/028b84186c17aab4bbb32fd07976b9367aee837c",
+ "reference": "028b84186c17aab4bbb32fd07976b9367aee837c",
"shasum": ""
},
"require": {
"composer-runtime-api": "^2.0",
"composer/semver": "^3.3",
- "php": "^8.0",
- "symfony/process": "^5.4 || ^6.0 || ^7.0"
+ "php": "^8.2",
+ "symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.8",
- "phpunit/phpunit": "^9.6 || ^10.0",
- "vimeo/psalm": "^5.9"
+ "mockery/mockery": "^1.6",
+ "spiral/code-style": "^2.3.1",
+ "testo/bridge-mockery": "^0.1.2",
+ "testo/testo": "^0.10.55",
+ "vimeo/psalm": "^6 || ^7"
},
"type": "library",
"autoload": {
@@ -1930,23 +1937,41 @@
"license": [
"MIT"
],
- "description": "The package for checking the version of the RoadRunner",
- "homepage": "https://github.com/roadrunner-php/version-checker",
+ "authors": [
+ {
+ "name": "Maksim Smakouz",
+ "homepage": "https://github.com/msmakouz"
+ },
+ {
+ "name": "Aleksei Gagarin",
+ "homepage": "https://github.com/roxblnfk"
+ },
+ {
+ "name": "RoadRunner Community",
+ "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors"
+ }
+ ],
+ "description": "Checks that the installed RoadRunner binary matches the version required by the project's Composer packages",
+ "homepage": "https://roadrunner.dev/",
"keywords": [
+ "compatibility",
"roadrunner",
- "roadrunner-php",
+ "semver",
+ "version",
"version-checker"
],
"support": {
- "source": "https://github.com/roadrunner-php/version-checker/tree/v1.0.2"
+ "chat": "https://discord.gg/V6EK4he",
+ "issues": "https://github.com/roadrunner-server/roadrunner/issues",
+ "source": "https://github.com/roadrunner-php/version-checker"
},
"funding": [
{
- "url": "https://github.com/roadrunner-server",
+ "url": "https://github.com/sponsors/roadrunner-server",
"type": "github"
}
],
- "time": "2025-05-20T08:45:05+00:00"
+ "time": "2026-10-10T13:07:13+00:00"
},
{
"name": "spiral/attributes",
@@ -2032,22 +2057,22 @@
},
{
"name": "spiral/core",
- "version": "3.16.0",
+ "version": "3.17.2",
"source": {
"type": "git",
"url": "https://github.com/spiral/core.git",
- "reference": "700db0337f7ba4d8ce87d102bc51c5820182edec"
+ "reference": "ca5b8483610aed0c82d4f06bc7907e191d88aca0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spiral/core/zipball/700db0337f7ba4d8ce87d102bc51c5820182edec",
- "reference": "700db0337f7ba4d8ce87d102bc51c5820182edec",
+ "url": "https://api.github.com/repos/spiral/core/zipball/ca5b8483610aed0c82d4f06bc7907e191d88aca0",
+ "reference": "ca5b8483610aed0c82d4f06bc7907e191d88aca0",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/container": "^1.1|^2.0",
- "spiral/security": "^3.16"
+ "spiral/security": "^3.17.2"
},
"provide": {
"psr/container-implementation": "^1.1|^2.0"
@@ -2060,7 +2085,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.16.x-dev"
+ "dev-master": "3.17.x-dev"
}
},
"autoload": {
@@ -2102,20 +2127,20 @@
"type": "github"
}
],
- "time": "2025-12-14T11:35:42+00:00"
+ "time": "2026-08-05T12:07:26+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 +2206,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 +2214,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.2",
"source": {
"type": "git",
"url": "https://github.com/spiral/hmvc.git",
- "reference": "cff7c60a72fd8a590f5d7e13a07244b39627aa5d"
+ "reference": "11111aac577f3d7ccc9bbe8b79dc6441fd21b9c5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spiral/hmvc/zipball/cff7c60a72fd8a590f5d7e13a07244b39627aa5d",
- "reference": "cff7c60a72fd8a590f5d7e13a07244b39627aa5d",
+ "url": "https://api.github.com/repos/spiral/hmvc/zipball/11111aac577f3d7ccc9bbe8b79dc6441fd21b9c5",
+ "reference": "11111aac577f3d7ccc9bbe8b79dc6441fd21b9c5",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/event-dispatcher": "^1.0",
- "spiral/core": "^3.16",
- "spiral/interceptors": "^3.16"
+ "spiral/core": "^3.17.2",
+ "spiral/interceptors": "^3.17.2"
},
"require-dev": {
"phpunit/phpunit": "^10.5.41",
@@ -2219,7 +2244,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.16.x-dev"
+ "dev-master": "3.17.x-dev"
}
},
"autoload": {
@@ -2263,26 +2288,26 @@
"type": "github"
}
],
- "time": "2025-12-14T11:38:02+00:00"
+ "time": "2026-08-05T12:00:27+00:00"
},
{
"name": "spiral/interceptors",
- "version": "3.16.0",
+ "version": "3.17.2",
"source": {
"type": "git",
"url": "https://github.com/spiral/interceptors.git",
- "reference": "715f274b35d10565f1c910eb573162179e33b0e7"
+ "reference": "ea0e10ecb2c6058541a2b097a660fd2b532a5ffa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spiral/interceptors/zipball/715f274b35d10565f1c910eb573162179e33b0e7",
- "reference": "715f274b35d10565f1c910eb573162179e33b0e7",
+ "url": "https://api.github.com/repos/spiral/interceptors/zipball/ea0e10ecb2c6058541a2b097a660fd2b532a5ffa",
+ "reference": "ea0e10ecb2c6058541a2b097a660fd2b532a5ffa",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/event-dispatcher": "^1.0",
- "spiral/core": "^3.16"
+ "spiral/core": "^3.17.2"
},
"require-dev": {
"phpunit/phpunit": "^10.5.41",
@@ -2292,7 +2317,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.16.x-dev"
+ "dev-master": "3.17.x-dev"
}
},
"autoload": {
@@ -2341,26 +2366,26 @@
"type": "github"
}
],
- "time": "2025-12-14T11:34:19+00:00"
+ "time": "2026-08-05T12:00:37+00:00"
},
{
"name": "spiral/logger",
- "version": "3.16.0",
+ "version": "3.17.2",
"source": {
"type": "git",
"url": "https://github.com/spiral/logger.git",
- "reference": "bd902bbcdbb6f247a51a75fc716dba3831966f06"
+ "reference": "671d91a3427019a14670b614474cc57156d5cd69"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spiral/logger/zipball/bd902bbcdbb6f247a51a75fc716dba3831966f06",
- "reference": "bd902bbcdbb6f247a51a75fc716dba3831966f06",
+ "url": "https://api.github.com/repos/spiral/logger/zipball/671d91a3427019a14670b614474cc57156d5cd69",
+ "reference": "671d91a3427019a14670b614474cc57156d5cd69",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/log": "1 - 3",
- "spiral/core": "^3.16"
+ "spiral/core": "^3.17.2"
},
"require-dev": {
"mockery/mockery": "^1.6.12",
@@ -2370,7 +2395,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.16.x-dev"
+ "dev-master": "3.17.x-dev"
}
},
"autoload": {
@@ -2412,20 +2437,20 @@
"type": "github"
}
],
- "time": "2025-12-14T11:36:52+00:00"
+ "time": "2026-08-05T12:05:38+00:00"
},
{
"name": "spiral/roadrunner",
- "version": "v2025.1.6",
+ "version": "v2025.1.15",
"source": {
"type": "git",
"url": "https://github.com/roadrunner-server/roadrunner.git",
- "reference": "207b2b4ba75f2529ecccf801b3d8dd7038f22732"
+ "reference": "321b817fab1056e404533ca1ddd200e77d1525fd"
},
"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/321b817fab1056e404533ca1ddd200e77d1525fd",
+ "reference": "321b817fab1056e404533ca1ddd200e77d1525fd",
"shasum": ""
},
"type": "metapackage",
@@ -2453,7 +2478,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.15"
},
"funding": [
{
@@ -2461,7 +2486,7 @@
"type": "github"
}
],
- "time": "2025-12-11T14:45:22+00:00"
+ "time": "2026-06-17T14:49:38+00:00"
},
{
"name": "spiral/roadrunner-cli",
@@ -2534,31 +2559,30 @@
},
{
"name": "spiral/roadrunner-kv",
- "version": "v4.3.1",
+ "version": "v4.4.0",
"source": {
"type": "git",
"url": "https://github.com/roadrunner-php/kv.git",
- "reference": "0db13f212c64463bd26cf71e28d0a8bdf997a4a0"
+ "reference": "c39d09cfa5c82a5a637c0f34a6f2411fa1c23b96"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/roadrunner-php/kv/zipball/0db13f212c64463bd26cf71e28d0a8bdf997a4a0",
- "reference": "0db13f212c64463bd26cf71e28d0a8bdf997a4a0",
+ "url": "https://api.github.com/repos/roadrunner-php/kv/zipball/c39d09cfa5c82a5a637c0f34a6f2411fa1c23b96",
+ "reference": "c39d09cfa5c82a5a637c0f34a6f2411fa1c23b96",
"shasum": ""
},
"require": {
"ext-json": "*",
- "php": ">=8.1",
+ "php": ">=8.2",
"psr/simple-cache": "2 - 3",
"roadrunner-php/roadrunner-api-dto": "^1.0",
"spiral/goridge": "^4.2",
"spiral/roadrunner": "^2023.1 || ^2024.1 || ^2025.1"
},
"require-dev": {
- "phpunit/phpunit": "^10.5.45",
- "roave/security-advisories": "dev-master",
- "spiral/code-style": "^2.2",
- "vimeo/psalm": ">=5.8"
+ "spiral/code-style": "^2.3.1",
+ "testo/testo": "^0.10.55",
+ "vimeo/psalm": "^6 || ^7"
},
"suggest": {
"ext-igbinary": "(>3.1.6) Igbinary serailizer support",
@@ -2606,7 +2630,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/kv/tree/v4.3.1"
+ "source": "https://github.com/roadrunner-php/kv/tree/v4.4.0"
},
"funding": [
{
@@ -2614,7 +2638,7 @@
"type": "github"
}
],
- "time": "2025-05-05T13:08:45+00:00"
+ "time": "2026-10-10T11:55:44+00:00"
},
{
"name": "spiral/roadrunner-worker",
@@ -2703,33 +2727,33 @@
},
{
"name": "spiral/security",
- "version": "3.16.0",
+ "version": "3.17.2",
"source": {
"type": "git",
"url": "https://github.com/spiral/security.git",
- "reference": "e47affcd2c1718209cae1fe0c3cf54c522db7b1a"
+ "reference": "aefd664efb3e618d0a57c967546f1ad5d9784580"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spiral/security/zipball/e47affcd2c1718209cae1fe0c3cf54c522db7b1a",
- "reference": "e47affcd2c1718209cae1fe0c3cf54c522db7b1a",
+ "url": "https://api.github.com/repos/spiral/security/zipball/aefd664efb3e618d0a57c967546f1ad5d9784580",
+ "reference": "aefd664efb3e618d0a57c967546f1ad5d9784580",
"shasum": ""
},
"require": {
"php": ">=8.1",
- "spiral/core": "^3.16",
- "spiral/hmvc": "^3.16"
+ "spiral/core": "^3.17.2",
+ "spiral/hmvc": "^3.17.2"
},
"require-dev": {
"mockery/mockery": "^1.6.12",
"phpunit/phpunit": "^10.5.41",
- "spiral/console": "^3.16",
+ "spiral/console": "^3.17.2",
"vimeo/psalm": "^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.16.x-dev"
+ "dev-master": "3.17.x-dev"
}
},
"autoload": {
@@ -2771,41 +2795,41 @@
"type": "github"
}
],
- "time": "2025-12-14T11:39:30+00:00"
+ "time": "2026-08-05T12:07:41+00:00"
},
{
"name": "spiral/tokenizer",
- "version": "3.16.0",
+ "version": "3.17.2",
"source": {
"type": "git",
"url": "https://github.com/spiral/tokenizer.git",
- "reference": "11611160800f959ff0ea329470b939a2c0898d1e"
+ "reference": "fcb7a36672d7aef900dc36377ff997ba6f8cde0b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spiral/tokenizer/zipball/11611160800f959ff0ea329470b939a2c0898d1e",
- "reference": "11611160800f959ff0ea329470b939a2c0898d1e",
+ "url": "https://api.github.com/repos/spiral/tokenizer/zipball/fcb7a36672d7aef900dc36377ff997ba6f8cde0b",
+ "reference": "fcb7a36672d7aef900dc36377ff997ba6f8cde0b",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": ">=8.1",
- "spiral/core": "^3.16",
- "spiral/logger": "^3.16",
+ "spiral/core": "^3.17.2",
+ "spiral/logger": "^3.17.2",
"symfony/finder": "^6.4.30 || ^7.4 || ^8.0"
},
"require-dev": {
"mockery/mockery": "^1.6.12",
"phpunit/phpunit": "^10.5.41",
"spiral/attributes": "^2.8|^3.0",
- "spiral/boot": "^3.16",
- "spiral/files": "^3.16",
+ "spiral/boot": "^3.17.2",
+ "spiral/files": "^3.17.2",
"vimeo/psalm": "^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.16.x-dev"
+ "dev-master": "3.17.x-dev"
}
},
"autoload": {
@@ -2847,24 +2871,24 @@
"type": "github"
}
],
- "time": "2025-12-14T11:39:35+00:00"
+ "time": "2026-08-05T12:05:29+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 +2928,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 +2948,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.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e"
+ "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e",
- "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e",
+ "url": "https://api.github.com/repos/symfony/console/zipball/d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d",
+ "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d",
"shasum": ""
},
"require": {
@@ -3002,7 +3026,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.4.1"
+ "source": "https://github.com/symfony/console/tree/v7.4.20"
},
"funding": [
{
@@ -3022,20 +3046,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-05T15:23:39+00:00"
+ "time": "2026-09-18T15:55:00+00:00"
},
{
"name": "symfony/deprecation-contracts",
- "version": "v3.6.0",
+ "version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
+ "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
},
"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/f3202fa1b5097b0af062dc978b32ecf63404e31d",
+ "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
"shasum": ""
},
"require": {
@@ -3048,7 +3072,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.6-dev"
+ "dev-main": "3.7-dev"
}
},
"autoload": {
@@ -3073,7 +3097,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.1"
},
"funding": [
{
@@ -3084,34 +3108,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-06-05T06:23:12+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v7.4.0",
+ "version": "v8.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "d551b38811096d0be9c4691d406991b47c0c630a"
+ "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a",
- "reference": "d551b38811096d0be9c4691d406991b47c0c630a",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/7599ebb855fede59413ddb7f15198d67bfcae7ba",
+ "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba",
"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 +3168,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.6"
},
"funding": [
{
@@ -3159,24 +3188,24 @@
"type": "tidelift"
}
],
- "time": "2025-11-27T13:27:24+00:00"
+ "time": "2026-08-23T10:06:25+00:00"
},
{
"name": "symfony/finder",
- "version": "v8.0.0",
+ "version": "v8.1.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291"
+ "reference": "bd4fed193a3132c91c5c93595cbfc78988915822"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291",
- "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/bd4fed193a3132c91c5c93595cbfc78988915822",
+ "reference": "bd4fed193a3132c91c5c93595cbfc78988915822",
"shasum": ""
},
"require": {
- "php": ">=8.4"
+ "php": ">=8.4.1"
},
"require-dev": {
"symfony/filesystem": "^7.4|^8.0"
@@ -3207,7 +3236,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.8"
},
"funding": [
{
@@ -3227,35 +3256,32 @@
"type": "tidelift"
}
],
- "time": "2025-11-05T14:36:47+00:00"
+ "time": "2026-09-26T06:33:49+00:00"
},
{
"name": "symfony/http-client",
- "version": "v7.4.1",
+ "version": "v8.1.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "26cc224ea7103dda90e9694d9e139a389092d007"
+ "reference": "e20aca4ceaa888197de5c792ffc551d99e43d98a"
},
"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/e20aca4ceaa888197de5c792ffc551d99e43d98a",
+ "reference": "e20aca4ceaa888197de5c792ffc551d99e43d98a",
"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 +3290,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|^8.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
- "symfony/amphp-http-client-meta": "^1.0|^2.0",
- "symfony/cache": "^6.4|^7.0|^8.0",
- "symfony/dependency-injection": "^6.4|^7.0|^8.0",
- "symfony/http-kernel": "^6.4|^7.0|^8.0",
- "symfony/messenger": "^6.4|^7.0|^8.0",
- "symfony/process": "^6.4|^7.0|^8.0",
- "symfony/rate-limiter": "^6.4|^7.0|^8.0",
- "symfony/stopwatch": "^6.4|^7.0|^8.0"
+ "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 +3333,7 @@
"http"
],
"support": {
- "source": "https://github.com/symfony/http-client/tree/v7.4.1"
+ "source": "https://github.com/symfony/http-client/tree/v8.1.8"
},
"funding": [
{
@@ -3328,20 +3353,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-04T21:12:57+00:00"
+ "time": "2026-09-24T11:09:32+00:00"
},
{
"name": "symfony/http-client-contracts",
- "version": "v3.6.0",
+ "version": "v3.7.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
- "reference": "75d7043853a42837e68111812f4d964b01e5101c"
+ "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f"
},
"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/35be0019e2c2c9fba80f9dc033290a5240f7b44f",
+ "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f",
"shasum": ""
},
"require": {
@@ -3354,7 +3379,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.6-dev"
+ "dev-main": "3.7-dev"
}
},
"autoload": {
@@ -3390,7 +3415,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.3"
},
"funding": [
{
@@ -3401,25 +3426,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-08-04T08:41:16+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 +3498,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 +3518,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.43.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
+ "reference": "5cac8f4db97208823aa353f59a01faad2da52c75"
},
"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/5cac8f4db97208823aa353f59a01faad2da52c75",
+ "reference": "5cac8f4db97208823aa353f59a01faad2da52c75",
"shasum": ""
},
"require": {
@@ -3551,7 +3580,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.43.0"
},
"funding": [
{
@@ -3571,20 +3600,20 @@
"type": "tidelift"
}
],
- "time": "2025-06-27T09:58:17+00:00"
+ "time": "2026-09-25T15:46:47+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.33.0",
+ "version": "v1.43.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
- "reference": "3833d7255cc303546435cb650316bff708a1c75c"
+ "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b"
},
"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/ebd57ccc7edb93d632e184efbe7693d2517bab3b",
+ "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b",
"shasum": ""
},
"require": {
@@ -3636,7 +3665,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.43.0"
},
"funding": [
{
@@ -3656,20 +3685,20 @@
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2026-09-23T22:02:09+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.33.0",
+ "version": "v1.43.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+ "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72"
},
"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/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72",
+ "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72",
"shasum": ""
},
"require": {
@@ -3721,7 +3750,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0"
},
"funding": [
{
@@ -3741,7 +3770,7 @@
"type": "tidelift"
}
],
- "time": "2024-12-23T08:48:59+00:00"
+ "time": "2026-09-24T15:19:04+00:00"
},
{
"name": "symfony/polyfill-php82",
@@ -3825,16 +3854,16 @@
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.33.0",
+ "version": "v1.43.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
- "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
+ "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce"
},
"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/80ccff923a8d61f73ebe9da0aa94e25232f004ce",
+ "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce",
"shasum": ""
},
"require": {
@@ -3881,7 +3910,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.43.0"
},
"funding": [
{
@@ -3901,24 +3930,24 @@
"type": "tidelift"
}
],
- "time": "2025-07-08T02:45:35+00:00"
+ "time": "2026-09-25T15:50:05+00:00"
},
{
"name": "symfony/process",
- "version": "v7.4.8",
+ "version": "v8.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "60f19cd3badc8de688421e21e4305eba50f8089a"
+ "reference": "10823b09358e690df4ff943e24e8492bb1019fc3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a",
- "reference": "60f19cd3badc8de688421e21e4305eba50f8089a",
+ "url": "https://api.github.com/repos/symfony/process/zipball/10823b09358e690df4ff943e24e8492bb1019fc3",
+ "reference": "10823b09358e690df4ff943e24e8492bb1019fc3",
"shasum": ""
},
"require": {
- "php": ">=8.2"
+ "php": ">=8.4.1"
},
"type": "library",
"autoload": {
@@ -3946,7 +3975,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.4.8"
+ "source": "https://github.com/symfony/process/tree/v8.1.7"
},
"funding": [
{
@@ -3966,20 +3995,20 @@
"type": "tidelift"
}
],
- "time": "2026-03-24T13:12:05+00:00"
+ "time": "2026-09-02T12:40:29+00:00"
},
{
"name": "symfony/service-contracts",
- "version": "v3.6.1",
+ "version": "v3.7.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
+ "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257"
},
"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/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
+ "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
"shasum": ""
},
"require": {
@@ -3997,7 +4026,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.6-dev"
+ "dev-main": "3.7-dev"
}
},
"autoload": {
@@ -4033,7 +4062,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
+ "source": "https://github.com/symfony/service-contracts/tree/v3.7.3"
},
"funding": [
{
@@ -4053,24 +4082,24 @@
"type": "tidelift"
}
],
- "time": "2025-07-15T11:30:57+00:00"
+ "time": "2026-07-27T15:39:01+00:00"
},
{
"name": "symfony/string",
- "version": "v8.0.1",
+ "version": "v8.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc"
+ "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc",
- "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc",
+ "url": "https://api.github.com/repos/symfony/string/zipball/d950140b5f56f31901e5b7a0c04ffc3a3deb943c",
+ "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c",
"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 +4152,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v8.0.1"
+ "source": "https://github.com/symfony/string/tree/v8.1.7"
},
"funding": [
{
@@ -4143,24 +4172,24 @@
"type": "tidelift"
}
],
- "time": "2025-12-01T09:13:36+00:00"
+ "time": "2026-09-11T14:51:16+00:00"
},
{
"name": "symfony/translation",
- "version": "v8.0.1",
+ "version": "v8.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "770e3b8b0ba8360958abedcabacd4203467333ca"
+ "reference": "d9e1caba0d6b6f9a26710af8a2f88d37f001215a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/770e3b8b0ba8360958abedcabacd4203467333ca",
- "reference": "770e3b8b0ba8360958abedcabacd4203467333ca",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/d9e1caba0d6b6f9a26710af8a2f88d37f001215a",
+ "reference": "d9e1caba0d6b6f9a26710af8a2f88d37f001215a",
"shasum": ""
},
"require": {
- "php": ">=8.4",
+ "php": ">=8.4.1",
"symfony/polyfill-mbstring": "^1.0",
"symfony/translation-contracts": "^3.6.1"
},
@@ -4216,7 +4245,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.5"
},
"funding": [
{
@@ -4236,20 +4265,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-01T09:13:36+00:00"
+ "time": "2026-08-21T17:47:34+00:00"
},
{
"name": "symfony/translation-contracts",
- "version": "v3.6.1",
+ "version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
- "reference": "65a8bc82080447fae78373aa10f8d13b38338977"
+ "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621"
},
"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/ccb206b98faccc511ebae8e5fad50f2dc0b30621",
+ "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621",
"shasum": ""
},
"require": {
@@ -4262,7 +4291,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.6-dev"
+ "dev-main": "3.7-dev"
}
},
"autoload": {
@@ -4298,7 +4327,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1"
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1"
},
"funding": [
{
@@ -4318,31 +4347,32 @@
"type": "tidelift"
}
],
- "time": "2025-07-15T13:41:35+00:00"
+ "time": "2026-06-05T06:23:12+00:00"
},
{
"name": "symfony/yaml",
- "version": "v8.0.1",
+ "version": "v8.1.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14"
+ "reference": "030e96eed0303223bfca3af9109e9e64830b6ec3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14",
- "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/030e96eed0303223bfca3af9109e9e64830b6ec3",
+ "reference": "030e96eed0303223bfca3af9109e9e64830b6ec3",
"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 +4403,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.8"
},
"funding": [
{
@@ -4393,7 +4423,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-04T18:17:06+00:00"
+ "time": "2026-09-26T06:33:49+00:00"
},
{
"name": "tbachert/spi",
@@ -4510,41 +4540,42 @@
},
{
"name": "temporal/sdk",
- "version": "v2.17.1",
+ "version": "dev-nexus-new",
"source": {
"type": "git",
"url": "https://github.com/temporalio/sdk-php.git",
- "reference": "cdbf6874efd25b3eee50b52c5c2d54ae0d004be4"
+ "reference": "fd037a33b6ae8934924787f029a0c14b1e47ead6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/cdbf6874efd25b3eee50b52c5c2d54ae0d004be4",
- "reference": "cdbf6874efd25b3eee50b52c5c2d54ae0d004be4",
+ "url": "https://api.github.com/repos/temporalio/sdk-php/zipball/fd037a33b6ae8934924787f029a0c14b1e47ead6",
+ "reference": "fd037a33b6ae8934924787f029a0c14b1e47ead6",
"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": "^3.4",
- "nesbot/carbon": "^2.72.6 || ^3.8.4",
- "php": ">=8.1",
+ "nesbot/carbon": "^3.8.4",
+ "php": ">=8.2",
+ "psr/clock": "^1.0",
"psr/log": "^2.0 || ^3.0.2",
"ramsey/uuid": "^4.7.6",
- "roadrunner-php/roadrunner-api-dto": "^1.14.0",
+ "roadrunner-php/roadrunner-api-dto": "^1.15.0",
"roadrunner-php/version-checker": "^1.0.2",
"spiral/attributes": "^3.1.8",
"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 || ^8.0",
- "symfony/http-client": "^5.4.49 || ^6.4.17 || ^7.0 || ^8.0",
+ "symfony/filesystem": "^6.4.24 || ^7.0 || ^8.0",
+ "symfony/http-client": "^6.4.17 || ^7.0 || ^8.0",
"symfony/polyfill-php83": "^1.31.0",
- "symfony/process": "^5.4.51 || ^6.4.15 || ^7.0 || ^8.0"
+ "symfony/process": "^6.4.33 || ^7.0 || ^8.0"
},
"require-dev": {
"buggregator/trap": "^1.13.0",
@@ -4553,10 +4584,11 @@
"dereuromark/composer-prefer-lowest": "^0.1.10",
"doctrine/annotations": "^1.14.4 || ^2.0.2",
"ext-simplexml": "*",
- "internal/dload": "^1.2.0",
+ "internal/dload": "^1.16.5",
"internal/toml": "^1.0.3",
"jetbrains/phpstorm-attributes": "dev-master",
"laminas/laminas-code": "^4.16",
+ "phpbench/phpbench": "^1.4",
"phpunit/phpunit": "10.5.63",
"spiral/code-style": "~2.3.0",
"spiral/core": "^3.14.9",
@@ -4574,23 +4606,87 @@
"extra": {
"patches": {
"phpunit/phpunit": {
- "Make runTest protected": "patches/phpunit-run-test-protected.patch",
- "Improve assertInstanceOf error message": "patches/phpunit-instance-of.patch"
+ "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"
+ ],
+ "bench": [
+ "phpbench run --report=aggregate"
+ ],
+ "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:func-timeskip": [
+ "tests/runner.php vendor/bin/phpunit --testsuite=Functional-TimeSkipping --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"
],
@@ -4607,12 +4703,12 @@
"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": "2026-03-08T14:43:31+00:00"
+ "time": "2026-10-13T07:40:22+00:00"
}
],
"packages-dev": [
@@ -5358,6 +5454,70 @@
},
"time": "2024-10-02T11:34:13+00:00"
},
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.2.8",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e285254e60f33c21902efef4a926ca0987c06804",
+ "reference": "e285254e60f33c21902efef4a926ca0987c06804",
+ "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-08-04T22:21:45+00:00"
+ },
{
"name": "phpunit/php-code-coverage",
"version": "10.1.16",
@@ -7102,9 +7262,18 @@
"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": {},
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..84b37f3 100644
--- a/app/phpunit.xml.dist
+++ b/app/phpunit.xml.dist
@@ -18,7 +18,8 @@
-
+
+
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/.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 @@
+ */
+ private object $sampleNexusService;
+
+ public function __construct()
+ {
+ $this->sampleNexusService = Workflow::newNexusServiceStub(
+ SampleNexusService::class,
+ NexusOperationOptions::new()
+ ->withEndpoint(CallerWorker::ENDPOINT_NAME)
+ ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)),
+ );
+ }
+
+ public function echo(string $message): \Generator
+ {
+ /** @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..da06a6d
--- /dev/null
+++ b/app/src/Nexus/Caller/HelloCallerWorkflow.php
@@ -0,0 +1,16 @@
+sampleNexusService = Workflow::newUntypedNexusOperationStub(
+ NexusOperationOptions::new()
+ ->withEndpoint(CallerWorker::ENDPOINT_NAME)
+ ->withService('SampleNexusService')
+ ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)),
+ );
+ }
+
+ public function hello(string $message, Language $language): \Generator
+ {
+ /** @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 $handle->getResult();
+
+ return $output->message;
+ }
+}
diff --git a/app/src/Nexus/ExecuteCommand.php b/app/src/Nexus/ExecuteCommand.php
new file mode 100644
index 0000000..e8c9b3b
--- /dev/null
+++ b/app/src/Nexus/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/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) {
+ Language::EN => new HelloOutput("Hello {$input->name} π"),
+ Language::FR => new HelloOutput("Bonjour {$input->name} π"),
+ Language::DE => new HelloOutput("Hallo {$input->name} π"),
+ Language::ES => new HelloOutput("Β‘Hola! {$input->name} π"),
+ Language::TR => new HelloOutput("Merhaba {$input->name} π"),
+ };
+ }
+}
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..7403634
--- /dev/null
+++ b/app/src/Nexus/README.md
@@ -0,0 +1,80 @@
+# 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)]`)
+
+Two caller workflows:
+
+- `EchoCallerWorkflow` β typed stub, sync op.
+- `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.
+
+## 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 π
+```
+
+## 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,
+ );
+
+$factory->run();
diff --git a/app/src/Nexus/handler-worker.php b/app/src/Nexus/handler-worker.php
new file mode 100644
index 0000000..ed1c30b
--- /dev/null
+++ b/app/src/Nexus/handler-worker.php
@@ -0,0 +1,39 @@
+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..ddbec40
--- /dev/null
+++ b/app/src/NexusCancellation/.rr.caller.yaml
@@ -0,0 +1,16 @@
+version: "3"
+
+rpc:
+ listen: tcp://127.0.0.1:6112
+
+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..60d43d8
--- /dev/null
+++ b/app/src/NexusCancellation/.rr.handler.yaml
@@ -0,0 +1,16 @@
+version: "3"
+
+rpc:
+ listen: tcp://127.0.0.1:6111
+
+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..b2236cb
--- /dev/null
+++ b/app/src/NexusCancellation/Caller/CallerWorker.php
@@ -0,0 +1,11 @@
+ */
+ private object $sampleNexusService;
+
+ public function __construct()
+ {
+ $this->sampleNexusService = Workflow::newNexusServiceStub(
+ SampleNexusService::class,
+ NexusOperationOptions::new()
+ ->withEndpoint(CallerWorker::ENDPOINT_NAME)
+ ->withScheduleToCloseTimeout(CarbonInterval::seconds(10))
+ ->withCancellationType(NexusOperationCancellationType::WaitRequested),
+ );
+ }
+
+ public function hello(string $message): \Generator
+ {
+ $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..a1fce96
--- /dev/null
+++ b/app/src/NexusCancellation/Handler/HandlerWorker.php
@@ -0,0 +1,10 @@
+ \random_int(0, 4))));
+
+ return match ($input->language) {
+ Language::EN => new HelloOutput("Hello {$input->name} π"),
+ Language::FR => new HelloOutput("Bonjour {$input->name} π"),
+ Language::DE => new HelloOutput("Hallo {$input->name} π"),
+ Language::ES => new HelloOutput("Β‘Hola! {$input->name} π"),
+ Language::TR => new HelloOutput("Merhaba {$input->name} π"),
+ };
+ } 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/README.md b/app/src/NexusCancellation/README.md
new file mode 100644
index 0000000..2fa27bf
--- /dev/null
+++ b/app/src/NexusCancellation/README.md
@@ -0,0 +1,72 @@
+# 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.
+
+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
+wait for the detached cleanup scope to finish on the handler side.
+
+## Prerequisites
+
+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
+
+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/caller-worker.php b/app/src/NexusCancellation/caller-worker.php
new file mode 100644
index 0000000..997dbc9
--- /dev/null
+++ b/app/src/NexusCancellation/caller-worker.php
@@ -0,0 +1,24 @@
+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..ce400c4
--- /dev/null
+++ b/app/src/NexusCancellation/handler-worker.php
@@ -0,0 +1,39 @@
+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..148da33
--- /dev/null
+++ b/app/src/NexusContextPropagation/.rr.caller.yaml
@@ -0,0 +1,16 @@
+version: "3"
+
+rpc:
+ listen: tcp://127.0.0.1:6122
+
+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..2b08d5e
--- /dev/null
+++ b/app/src/NexusContextPropagation/.rr.handler.yaml
@@ -0,0 +1,16 @@
+version: "3"
+
+rpc:
+ listen: tcp://127.0.0.1:6121
+
+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..b7c9471
--- /dev/null
+++ b/app/src/NexusContextPropagation/Caller/CallerWorker.php
@@ -0,0 +1,11 @@
+ */
+ private object $sampleNexusService;
+
+ public function __construct()
+ {
+ $this->sampleNexusService = Workflow::newNexusServiceStub(
+ SampleNexusService::class,
+ NexusOperationOptions::new()
+ ->withEndpoint(CallerWorker::ENDPOINT_NAME)
+ ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)),
+ );
+ }
+
+ public function echo(string $message): \Generator
+ {
+ 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/HelloCallerWorkflowImpl.php b/app/src/NexusContextPropagation/Caller/HelloCallerWorkflowImpl.php
new file mode 100644
index 0000000..5f0be9e
--- /dev/null
+++ b/app/src/NexusContextPropagation/Caller/HelloCallerWorkflowImpl.php
@@ -0,0 +1,41 @@
+ */
+ private object $sampleNexusService;
+
+ public function __construct()
+ {
+ $this->sampleNexusService = Workflow::newNexusServiceStub(
+ SampleNexusService::class,
+ NexusOperationOptions::new()
+ ->withEndpoint(CallerWorker::ENDPOINT_NAME)
+ ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)),
+ );
+ }
+
+ public function hello(string $message, Language $language): \Generator
+ {
+ 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..b3cf669
--- /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/HandlerWorker.php b/app/src/NexusContextPropagation/Handler/HandlerWorker.php
new file mode 100644
index 0000000..226ef22
--- /dev/null
+++ b/app/src/NexusContextPropagation/Handler/HandlerWorker.php
@@ -0,0 +1,10 @@
+name
+ : $input->name . ', x-nexus-caller-workflow-id: ' . $callerWorkflowId;
+
+ return match ($input->language) {
+ Language::EN => new HelloOutput("Hello {$name} π"),
+ Language::FR => new HelloOutput("Bonjour {$name} π"),
+ Language::DE => new HelloOutput("Hallo {$name} π"),
+ Language::ES => new HelloOutput("Β‘Hola! {$name} π"),
+ Language::TR => new HelloOutput("Merhaba {$name} π"),
+ };
+ }
+}
diff --git a/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php b/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php
new file mode 100644
index 0000000..ec22e62
--- /dev/null
+++ b/app/src/NexusContextPropagation/Handler/SampleNexusServiceImpl.php
@@ -0,0 +1,49 @@
+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..e8cc3c2
--- /dev/null
+++ b/app/src/NexusContextPropagation/Propagation/MDC.php
@@ -0,0 +1,39 @@
+> */
+ private static array $contexts = [];
+
+ public static function put(string $key, string $value): void
+ {
+ self::$contexts[self::scope()][$key] = $value;
+ }
+
+ public static function get(string $key): ?string
+ {
+ return self::$contexts[self::scope()][$key] ?? null;
+ }
+
+ /** @return array */
+ public static function getAll(): array
+ {
+ return self::$contexts[self::scope()] ?? [];
+ }
+
+ public static function clear(): void
+ {
+ unset(self::$contexts[self::scope()]);
+ }
+
+ private static function scope(): string
+ {
+ return Workflow::getInfo()->execution->getID();
+ }
+}
diff --git a/app/src/NexusContextPropagation/Propagation/NexusOutboundContextInterceptor.php b/app/src/NexusContextPropagation/Propagation/NexusOutboundContextInterceptor.php
new file mode 100644
index 0000000..43cf2a6
--- /dev/null
+++ b/app/src/NexusContextPropagation/Propagation/NexusOutboundContextInterceptor.php
@@ -0,0 +1,33 @@
+ $next
+ * @return PromiseInterface
+ */
+ public function executeNexusOperation(ExecuteNexusOperationInput $input, callable $next): PromiseInterface
+ {
+ $headers = $input->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/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
new file mode 100644
index 0000000..609119d
--- /dev/null
+++ b/app/src/NexusContextPropagation/README.md
@@ -0,0 +1,94 @@
+# Nexus Context Propagation sample
+
+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
+> 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
+
+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
+
+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/caller-worker.php b/app/src/NexusContextPropagation/caller-worker.php
new file mode 100644
index 0000000..2c382a5
--- /dev/null
+++ b/app/src/NexusContextPropagation/caller-worker.php
@@ -0,0 +1,34 @@
+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..c66104e
--- /dev/null
+++ b/app/src/NexusContextPropagation/handler-worker.php
@@ -0,0 +1,46 @@
+withNamespace($namespace),
+ interceptorProvider: new SimplePipelineProvider([new NexusStartContextInterceptor()]),
+);
+
+$factory = WorkerFactory::create(client: $workflowClient);
+
+$worker = $factory->newWorker(
+ HandlerWorker::TASK_QUEUE,
+ interceptorProvider: new SimplePipelineProvider([new WorkflowInboundContextInterceptor()]),
+);
+$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..bd2aa62
--- /dev/null
+++ b/app/src/NexusManualOperation/.rr.caller.yaml
@@ -0,0 +1,16 @@
+version: "3"
+
+rpc:
+ listen: tcp://127.0.0.1:6142
+
+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..1319e79
--- /dev/null
+++ b/app/src/NexusManualOperation/.rr.handler.yaml
@@ -0,0 +1,16 @@
+version: "3"
+
+rpc:
+ listen: tcp://127.0.0.1:6141
+
+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..6be1a11
--- /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;
+ $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, &$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/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..8186a04
--- /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..a2bdbd1
--- /dev/null
+++ b/app/src/NexusManualOperation/README.md
@@ -0,0 +1,96 @@
+# 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-manual-nexus-endpoint \
+ --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
+
+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-manual-nexus-endpoint` |
+| 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..d82b5cc
--- /dev/null
+++ b/app/src/NexusMultipleArguments/.rr.caller.yaml
@@ -0,0 +1,16 @@
+version: "3"
+
+rpc:
+ listen: tcp://127.0.0.1:6132
+
+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..ac98e47
--- /dev/null
+++ b/app/src/NexusMultipleArguments/.rr.handler.yaml
@@ -0,0 +1,16 @@
+version: "3"
+
+rpc:
+ listen: tcp://127.0.0.1:6131
+
+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..0eccfb7
--- /dev/null
+++ b/app/src/NexusMultipleArguments/Caller/CallerWorker.php
@@ -0,0 +1,11 @@
+ */
+ private object $sampleNexusService;
+
+ public function __construct()
+ {
+ $this->sampleNexusService = Workflow::newNexusServiceStub(
+ SampleNexusService::class,
+ NexusOperationOptions::new()
+ ->withEndpoint(CallerWorker::ENDPOINT_NAME)
+ ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)),
+ );
+ }
+
+ public function echo(string $message): \Generator
+ {
+ /** @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..50abcd2
--- /dev/null
+++ b/app/src/NexusMultipleArguments/Caller/HelloCallerWorkflow.php
@@ -0,0 +1,16 @@
+ */
+ private object $sampleNexusService;
+
+ public function __construct()
+ {
+ $this->sampleNexusService = Workflow::newNexusServiceStub(
+ SampleNexusService::class,
+ NexusOperationOptions::new()
+ ->withEndpoint(CallerWorker::ENDPOINT_NAME)
+ ->withScheduleToCloseTimeout(CarbonInterval::seconds(10)),
+ );
+ }
+
+ public function hello(string $message, Language $language): \Generator
+ {
+ /** @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..9cdd320
--- /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/HandlerWorker.php b/app/src/NexusMultipleArguments/Handler/HandlerWorker.php
new file mode 100644
index 0000000..e3e63c6
--- /dev/null
+++ b/app/src/NexusMultipleArguments/Handler/HandlerWorker.php
@@ -0,0 +1,10 @@
+ new HelloOutput("Hello {$name} π"),
+ Language::FR => new HelloOutput("Bonjour {$name} π"),
+ Language::DE => new HelloOutput("Hallo {$name} π"),
+ Language::ES => new HelloOutput("Β‘Hola! {$name} π"),
+ Language::TR => new HelloOutput("Merhaba {$name} π"),
+ };
+ }
+}
diff --git a/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php b/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php
new file mode 100644
index 0000000..d237de8
--- /dev/null
+++ b/app/src/NexusMultipleArguments/Handler/SampleNexusServiceImpl.php
@@ -0,0 +1,40 @@
+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..982fd4c
--- /dev/null
+++ b/app/src/NexusMultipleArguments/README.md
@@ -0,0 +1,126 @@
+# 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.
+
+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
+
+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
+
+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-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/caller-worker.php b/app/src/NexusMultipleArguments/caller-worker.php
new file mode 100644
index 0000000..be655c6
--- /dev/null
+++ b/app/src/NexusMultipleArguments/caller-worker.php
@@ -0,0 +1,28 @@
+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..490ecc4
--- /dev/null
+++ b/app/src/NexusMultipleArguments/handler-worker.php
@@ -0,0 +1,39 @@
+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..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
@@ -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..2a00b5b
--- /dev/null
+++ b/app/tests/Feature/Nexus/CallerWorkflowMockTest.php
@@ -0,0 +1,41 @@
+newCaller(EchoCallerWorkflow::class);
+
+ $result = $workflow->echo('ignored');
+
+ self::assertSame(MockEchoClient::CANNED, $result);
+ }
+
+ public function testHelloUsesMockHandlerWorkflow(): void
+ {
+ $workflow = $this->newCaller(HelloCallerWorkflow::class);
+
+ $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
new file mode 100644
index 0000000..b47cf59
--- /dev/null
+++ b/app/tests/Feature/Nexus/CallerWorkflowTest.php
@@ -0,0 +1,49 @@
+newCaller(EchoCallerWorkflow::class);
+
+ $result = $workflow->echo('Hello');
+
+ self::assertSame('Hello', $result);
+ }
+
+ public function testHelloRoundTrip(): void
+ {
+ $workflow = $this->newCaller(HelloCallerWorkflow::class);
+
+ $result = $workflow->hello('World', Language::EN);
+
+ self::assertSame('Hello World π', $result);
+ }
+
+ public function testHelloRespectsLanguage(): void
+ {
+ $workflow = $this->newCaller(HelloCallerWorkflow::class);
+
+ $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
new file mode 100644
index 0000000..0ebd36a
--- /dev/null
+++ b/app/tests/Feature/Nexus/CancellationTest.php
@@ -0,0 +1,46 @@
+newCaller(HelloCallerWorkflow::class);
+ $run = $this->workflowClient->start($workflow, 'Nexus');
+
+ 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) {
+ if ($event->hasNexusOperationCancelRequestedEventAttributes()) {
+ ++$count;
+ }
+ }
+
+ return $count;
+ }
+}
diff --git a/app/tests/Feature/Nexus/ContextPropagationTest.php b/app/tests/Feature/Nexus/ContextPropagationTest.php
new file mode 100644
index 0000000..ea7cfa7
--- /dev/null
+++ b/app/tests/Feature/Nexus/ContextPropagationTest.php
@@ -0,0 +1,49 @@
+newCaller(
+ EchoCallerWorkflow::class,
+ WorkflowOptions::new()->withWorkflowId($workflowId),
+ );
+
+ $result = $workflow->echo('ignored');
+
+ self::assertSame($workflowId, $result);
+ }
+
+ public function testCallerWorkflowIdReachesTheBackingWorkflow(): void
+ {
+ $workflowId = 'ctx-prop-async-' . \bin2hex(\random_bytes(4));
+ $workflow = $this->newCaller(
+ HelloCallerWorkflow::class,
+ WorkflowOptions::new()->withWorkflowId($workflowId),
+ );
+
+ $run = $this->workflowClient->start($workflow, 'Nexus', Language::EN);
+
+ self::assertStringContainsString($workflowId, $run->getResult('string'));
+ }
+}
diff --git a/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php b/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php
new file mode 100644
index 0000000..c672447
--- /dev/null
+++ b/app/tests/Feature/Nexus/ManualOperationStartFailureTest.php
@@ -0,0 +1,40 @@
+newCaller(
+ JobCallerWorkflow::class,
+ WorkflowOptions::new()->withWorkflowExecutionTimeout(CarbonInterval::seconds(15)),
+ );
+
+ try {
+ $workflow->run('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/ManualOperationTest.php b/app/tests/Feature/Nexus/ManualOperationTest.php
new file mode 100644
index 0000000..adfc0fa
--- /dev/null
+++ b/app/tests/Feature/Nexus/ManualOperationTest.php
@@ -0,0 +1,31 @@
+newCaller(JobCallerWorkflow::class);
+
+ $result = $workflow->run('Demo');
+
+ self::assertStringContainsString('done instantly: Demo', $result);
+ self::assertStringContainsString('[token=job-', $result);
+ self::assertStringEndsWith('cancelled', $result);
+ }
+}
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 @@
+';
+
+ 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..2391f69
--- /dev/null
+++ b/app/tests/Feature/Nexus/MultipleArgumentsTest.php
@@ -0,0 +1,28 @@
+newCaller(HelloCallerWorkflow::class);
+
+ $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
new file mode 100644
index 0000000..a89bb52
--- /dev/null
+++ b/app/tests/Feature/Nexus/NexusEndpointHelper.php
@@ -0,0 +1,138 @@
+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 $name = null): array
+ {
+ $name ??= 'samples-test-' . \bin2hex(\random_bytes(4));
+ $this->deleteEndpointByName($name);
+
+ $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}",
+ );
+ }
+
+ \assert($response instanceof CreateNexusEndpointResponse);
+
+ $id = $response->getEndpoint()->getId();
+ $this->awaitEndpointResolvable($id);
+
+ 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();
+ }
+
+ /**
+ * 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;
+ }
+
+ \assert($response instanceof ListNexusEndpointsResponse);
+
+ foreach ($response->getEndpoints() as $endpoint) {
+ $this->deleteEndpoint($endpoint->getId(), $endpoint->getVersion());
+ }
+ }
+
+ 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..a5bc621
--- /dev/null
+++ b/app/tests/Feature/Nexus/NexusServiceMockTest.php
@@ -0,0 +1,40 @@
+newCaller(EchoCallerWorkflow::class);
+
+ $result = $workflow->echo('ignored');
+
+ self::assertSame(MockSampleNexusServiceImpl::ECHO_CANNED, $result);
+ }
+
+ public function testHelloFromMockedService(): void
+ {
+ $workflow = $this->newCaller(HelloCallerWorkflow::class);
+
+ $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
new file mode 100644
index 0000000..1ced641
--- /dev/null
+++ b/app/tests/Feature/Nexus/NexusTestCase.php
@@ -0,0 +1,82 @@
+setupEndpoint(
+ namespace: 'default',
+ taskQueue: static::TASK_QUEUE,
+ name: static::ENDPOINT_NAME === '' ? null : static::ENDPOINT_NAME,
+ );
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ 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;
+ }
+
+ /**
+ * @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()->withWorkflowExecutionTimeout(CarbonInterval::seconds(60)))
+ ->withTaskQueue(static::TASK_QUEUE),
+ );
+ }
+}
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..62dfea9 100644
--- a/app/tests/Feature/bootstrap.php
+++ b/app/tests/Feature/bootstrap.php
@@ -12,10 +12,23 @@
$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.
+$httpPort = getenv('TEMPORAL_HTTP_PORT') ?: '7246';
+
+$environment->startTemporalServer(
+ parameters: [
+ '--http-port', $httpPort,
+ ],
+);
+// 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..602a019 100644
--- a/app/tests/Feature/worker.php
+++ b/app/tests/Feature/worker.php
@@ -2,6 +2,27 @@
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\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;
+use App\Tests\Feature\Nexus\Mock\MockSampleNexusServiceImpl;
+use App\Tests\Feature\Nexus\MultipleArgumentsTest;
+use App\Tests\Feature\Nexus\NexusServiceMockTest;
+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\Samples\NexusContextPropagation\Propagation\NexusStartContextInterceptor;
+use Temporal\Samples\NexusContextPropagation\Propagation\WorkflowInboundContextInterceptor;
use Temporal\Testing\WorkerFactory;
ini_set('display_errors', 'stderr');
@@ -9,15 +30,109 @@
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'),
+ interceptorProvider: new SimplePipelineProvider([new NexusStartContextInterceptor()]),
+);
-$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(
+ \Temporal\Samples\Nexus\Caller\EchoCallerWorkflowImpl::class,
+ \Temporal\Samples\Nexus\Caller\HelloCallerWorkflowImpl::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(
+ \Temporal\Samples\Nexus\Caller\EchoCallerWorkflowImpl::class,
+ \Temporal\Samples\Nexus\Caller\HelloCallerWorkflowImpl::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(
+ \Temporal\Samples\Nexus\Caller\EchoCallerWorkflowImpl::class,
+ \Temporal\Samples\Nexus\Caller\HelloCallerWorkflowImpl::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(
+ \Temporal\Samples\NexusCancellation\Caller\HelloCallerWorkflowImpl::class,
+ \Temporal\Samples\NexusCancellation\Handler\HelloHandlerWorkflowImpl::class,
+ )
+ ->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
+// handler side so the test can observe the propagated value.
+$workerFactory->newWorker(
+ taskQueue: ContextPropagationTest::TASK_QUEUE,
+ interceptorProvider: new SimplePipelineProvider([
+ new NexusOutboundContextInterceptor(),
+ new WorkflowInboundContextInterceptor(),
+ ]),
+)
+ ->registerWorkflowTypes(
+ \Temporal\Samples\NexusContextPropagation\Caller\EchoCallerWorkflowImpl::class,
+ \Temporal\Samples\NexusContextPropagation\Caller\HelloCallerWorkflowImpl::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(
+ \Temporal\Samples\NexusMultipleArguments\Caller\HelloCallerWorkflowImpl::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(\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(\Temporal\Samples\NexusManualOperation\Caller\JobCallerWorkflowImpl::class)
+ ->registerNexusServiceImplementation(
+ new \Temporal\Samples\NexusManualOperation\Handler\SampleNexusService(new FailingJobClient()),
+ );
$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];
}
}