From 68646e03bb3f15fef0e698894329d72e91e90308 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Wed, 24 Dec 2025 11:09:48 +0400 Subject: [PATCH 1/6] feat: support raw value --- src/DataConverter/DataConverter.php | 3 +- src/DataConverter/EncodingKeys.php | 1 + src/DataConverter/RawValue.php | 29 ++++++ src/DataConverter/RawValueConverter.php | 44 +++++++++ .../Acceptance/App/Feature/ClientFactory.php | 2 + .../Harness/DataConverter/RawValueTest.php | 98 +++++++++++++++++++ tests/Acceptance/worker.php | 2 + .../DataConverter/RawValueConverterTest.php | 54 ++++++++++ 8 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/DataConverter/RawValue.php create mode 100644 src/DataConverter/RawValueConverter.php create mode 100644 tests/Acceptance/Harness/DataConverter/RawValueTest.php create mode 100644 tests/Unit/DataConverter/RawValueConverterTest.php diff --git a/src/DataConverter/DataConverter.php b/src/DataConverter/DataConverter.php index d3ea48b55..761f8b67c 100644 --- a/src/DataConverter/DataConverter.php +++ b/src/DataConverter/DataConverter.php @@ -33,6 +33,7 @@ public static function createDefault(): DataConverterInterface return new DataConverter( new NullConverter(), new BinaryConverter(), + new RawValueConverter(), new ProtoJsonConverter(), new ProtoConverter(), new JsonConverter(), @@ -47,7 +48,7 @@ public function fromPayload(Payload $payload, $type) $encoding = $meta[EncodingKeys::METADATA_ENCODING_KEY]; if (!isset($this->converters[$encoding])) { - throw new DataConverterException(\sprintf('Undefined payload encoding %s', $encoding)); + throw new DataConverterException(\sprintf('Undefined payload encoding "%s"', $encoding)); } $type = Type::create($type); diff --git a/src/DataConverter/EncodingKeys.php b/src/DataConverter/EncodingKeys.php index a858c1818..1d84a294d 100644 --- a/src/DataConverter/EncodingKeys.php +++ b/src/DataConverter/EncodingKeys.php @@ -17,6 +17,7 @@ final class EncodingKeys public const METADATA_MESSAGE_TYPE = 'messageType'; public const METADATA_ENCODING_NULL = 'binary/null'; public const METADATA_ENCODING_RAW = 'binary/plain'; + public const METADATA_ENCODING_RAW_VALUE = 'binary'; public const METADATA_ENCODING_JSON = 'json/plain'; public const METADATA_ENCODING_PROTOBUF_JSON = 'json/protobuf'; public const METADATA_ENCODING_PROTOBUF = 'binary/protobuf'; diff --git a/src/DataConverter/RawValue.php b/src/DataConverter/RawValue.php new file mode 100644 index 000000000..9cac7ec2e --- /dev/null +++ b/src/DataConverter/RawValue.php @@ -0,0 +1,29 @@ +payload = $data; + } + + public function getPayload(): Payload + { + return $this->payload; + } +} diff --git a/src/DataConverter/RawValueConverter.php b/src/DataConverter/RawValueConverter.php new file mode 100644 index 000000000..4cc93dc4f --- /dev/null +++ b/src/DataConverter/RawValueConverter.php @@ -0,0 +1,44 @@ +getPayload(); + $payload->setMetadata([EncodingKeys::METADATA_ENCODING_KEY => EncodingKeys::METADATA_ENCODING_RAW_VALUE]); + + return $payload; + } + + public function fromPayload(Payload $payload, Type $type): RawValue + { + if (!$type->isClass() || $type->getName() !== RawValue::class) { + throw new DataConverterException(\sprintf('Unable to convert raw data to non %s type', RawValue::class)); + } + + return new RawValue($payload); + } +} diff --git a/tests/Acceptance/App/Feature/ClientFactory.php b/tests/Acceptance/App/Feature/ClientFactory.php index d645f3e30..5f6dddf4c 100644 --- a/tests/Acceptance/App/Feature/ClientFactory.php +++ b/tests/Acceptance/App/Feature/ClientFactory.php @@ -4,6 +4,7 @@ namespace Temporal\Tests\Acceptance\App\Feature; +use Temporal\DataConverter\RawValueConverter; use Temporal\Tests\Acceptance\App\Attribute\Client; use Temporal\Tests\Acceptance\App\Runtime\State; use Psr\Container\ContainerInterface; @@ -52,6 +53,7 @@ public function workflowClient(\ReflectionParameter $context): WorkflowClientInt if ($attribute->payloadConverters !== []) { $converters = [ new NullConverter(), + new RawValueConverter(), new BinaryConverter(), new ProtoConverter(), new ProtoJsonConverter(), diff --git a/tests/Acceptance/Harness/DataConverter/RawValueTest.php b/tests/Acceptance/Harness/DataConverter/RawValueTest.php new file mode 100644 index 000000000..ff5914653 --- /dev/null +++ b/tests/Acceptance/Harness/DataConverter/RawValueTest.php @@ -0,0 +1,98 @@ +interceptor]); + } + + #[Test] + public function check( + #[Stub('Harness_DataConverter_RawValue')] + #[Client(pipelineProvider: [self::class, 'pipelineProvider'])] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult(RawValue::class); + + self::assertInstanceOf(RawValue::class, $result); + self::assertInstanceOf(Payload::class, $result->getPayload()); + self::assertSame('hello world', $result->getPayload()->getData()); + + # Check arguments + self::assertNotNull($this->interceptor->startRequest); + self::assertNotNull($this->interceptor->result); + + /** @var Payload $payload */ + $payload = $this->interceptor->startRequest->getInput()?->getPayloads()[0] ?? null; + self::assertNotNull($payload); + + self::assertSame(EncodingKeys::METADATA_ENCODING_RAW_VALUE, $payload->getMetadata()['encoding']); + + // Check result value from interceptor + /** @var Payload $resultPayload */ + $resultPayload = $this->interceptor->result->toPayloads()->getPayloads()[0]; + self::assertSame(EncodingKeys::METADATA_ENCODING_RAW_VALUE, $resultPayload->getMetadata()['encoding']); + } + + protected function setUp(): void + { + $this->interceptor = new Interceptor(); + parent::setUp(); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_DataConverter_RawValue')] + public function run() + { + return yield new RawValue(new Payload(['data' => 'hello world'])); + } +} + +class Interceptor implements GrpcClientInterceptor, WorkflowClientCallsInterceptor +{ + use WorkflowClientCallsInterceptorTrait; + + public ?StartWorkflowExecutionRequest $startRequest = null; + public ?EncodedValues $result = null; + + public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object + { + $arg instanceof StartWorkflowExecutionRequest and $this->startRequest = $arg; + return $next($method, $arg, $ctx); + } + + public function getResult(GetResultInput $input, callable $next): ?EncodedValues + { + return $this->result = $next($input); + } +} diff --git a/tests/Acceptance/worker.php b/tests/Acceptance/worker.php index 15f2f9582..bd473f80e 100644 --- a/tests/Acceptance/worker.php +++ b/tests/Acceptance/worker.php @@ -21,6 +21,7 @@ use Temporal\DataConverter\NullConverter; use Temporal\DataConverter\ProtoConverter; use Temporal\DataConverter\ProtoJsonConverter; +use Temporal\DataConverter\RawValueConverter; use Temporal\Internal\Support\StackRenderer; use Temporal\Testing\Command; use Temporal\Tests\Acceptance\App\Runtime\Feature; @@ -52,6 +53,7 @@ $converters = [ new NullConverter(), new BinaryConverter(), + new RawValueConverter(), new ProtoJsonConverter(), new ProtoConverter(), new JsonConverter(), diff --git a/tests/Unit/DataConverter/RawValueConverterTest.php b/tests/Unit/DataConverter/RawValueConverterTest.php new file mode 100644 index 000000000..283618c1b --- /dev/null +++ b/tests/Unit/DataConverter/RawValueConverterTest.php @@ -0,0 +1,54 @@ + 1]); + $message = new RawValue($innerPayload); + + $payload = DataConverter::createDefault()->toPayload($message); + + self::assertSame($innerPayload, $payload); + self::assertSame(EncodingKeys::METADATA_ENCODING_RAW_VALUE, $payload->getMetadata()[EncodingKeys::METADATA_ENCODING_KEY]); + } + + public function testRawPayloadDecoding(): void + { + $innerPayload = new Payload(['data' => 1]); + $message = new RawValue($innerPayload); + + $encoded = DataConverter::createDefault()->toPayload($message); + $decoded = DataConverter::createDefault()->fromPayload($encoded, RawValue::class); + + self::assertInstanceOf(RawValue::class, $decoded); + self::assertSame($decoded->getPayload(), $encoded); + } + + protected function create(): DataConverterInterface + { + return DataConverter::createDefault(); + } +} From 0fb588cf483016f2b82da886729545b61de6395a Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 25 Dec 2025 15:34:57 +0400 Subject: [PATCH 2/6] feat: simplify checks --- src/DataConverter/DataConverter.php | 15 +++++++-------- src/DataConverter/Type.php | 27 ++++++++------------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/DataConverter/DataConverter.php b/src/DataConverter/DataConverter.php index 761f8b67c..09e1dacd0 100644 --- a/src/DataConverter/DataConverter.php +++ b/src/DataConverter/DataConverter.php @@ -52,15 +52,14 @@ public function fromPayload(Payload $payload, $type) } $type = Type::create($type); - if (\in_array($type->getName(), [Type::TYPE_VOID, Type::TYPE_NULL, Type::TYPE_FALSE, Type::TYPE_TRUE], true)) { - return match ($type->getName()) { - Type::TYPE_VOID, Type::TYPE_NULL => null, - Type::TYPE_TRUE => true, - Type::TYPE_FALSE => false, - }; - } - return $this->converters[$encoding]->fromPayload($payload, $type); + return match ($type->getName()) { + Type::TYPE_VOID, + Type::TYPE_NULL => null, + Type::TYPE_TRUE => true, + Type::TYPE_FALSE => false, + default => $this->converters[$encoding]->fromPayload($payload, $type), + }; } /** diff --git a/src/DataConverter/Type.php b/src/DataConverter/Type.php index 70af788ee..1ecb30567 100644 --- a/src/DataConverter/Type.php +++ b/src/DataConverter/Type.php @@ -79,25 +79,14 @@ public static function fromReflectionType(\ReflectionType $type): self */ public static function create($type): Type { - switch (true) { - case $type instanceof ReturnType: - return new self($type->name, $type->nullable); - - case $type instanceof self: - return $type; - - case \is_string($type): - return new self($type); - - case $type instanceof \ReflectionClass: - return self::fromReflectionClass($type); - - case $type instanceof \ReflectionType: - return self::fromReflectionType($type); - - default: - return new self(); - } + return match (true) { + $type instanceof ReturnType => new self($type->name, $type->nullable), + $type instanceof self => $type, + \is_string($type) => new self($type), + $type instanceof \ReflectionClass => self::fromReflectionClass($type), + $type instanceof \ReflectionType => self::fromReflectionType($type), + default => new self(), + }; } public function getName(): string From f39a9f43cafcd13a95407cc58aec6c56d2dd6f63 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 25 Dec 2025 15:35:19 +0400 Subject: [PATCH 3/6] feat: move test from harness to extra namespace --- .../Extra/DataConverter/RawValueTest.php | 62 ++++++++++++ .../Harness/DataConverter/RawValueTest.php | 98 ------------------- 2 files changed, 62 insertions(+), 98 deletions(-) create mode 100644 tests/Acceptance/Extra/DataConverter/RawValueTest.php delete mode 100644 tests/Acceptance/Harness/DataConverter/RawValueTest.php diff --git a/tests/Acceptance/Extra/DataConverter/RawValueTest.php b/tests/Acceptance/Extra/DataConverter/RawValueTest.php new file mode 100644 index 000000000..68e18946b --- /dev/null +++ b/tests/Acceptance/Extra/DataConverter/RawValueTest.php @@ -0,0 +1,62 @@ +getResult(RawValue::class); + + self::assertInstanceOf(RawValue::class, $result); + self::assertInstanceOf(Payload::class, $result->getPayload()); + self::assertSame('hello world', $result->getPayload()->getData()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Extra_DataConverter_RawValue')] + public function run() + { + $rawValue = new RawValue(new Payload(['data' => 'hello world'])); + + yield Workflow::newActivityStub( + RawValueActivity::class, + ActivityOptions::new() + ->withStartToCloseTimeout(10), + ) + ->bypass($rawValue); + + return yield $rawValue; + } +} + +#[ActivityInterface(prefix: 'RawValueActivity.')] +class RawValueActivity +{ + #[ActivityMethod('Bypass')] + public function bypass(RawValue $arg): iterable + { + yield $arg; + } +} diff --git a/tests/Acceptance/Harness/DataConverter/RawValueTest.php b/tests/Acceptance/Harness/DataConverter/RawValueTest.php deleted file mode 100644 index ff5914653..000000000 --- a/tests/Acceptance/Harness/DataConverter/RawValueTest.php +++ /dev/null @@ -1,98 +0,0 @@ -interceptor]); - } - - #[Test] - public function check( - #[Stub('Harness_DataConverter_RawValue')] - #[Client(pipelineProvider: [self::class, 'pipelineProvider'])] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult(RawValue::class); - - self::assertInstanceOf(RawValue::class, $result); - self::assertInstanceOf(Payload::class, $result->getPayload()); - self::assertSame('hello world', $result->getPayload()->getData()); - - # Check arguments - self::assertNotNull($this->interceptor->startRequest); - self::assertNotNull($this->interceptor->result); - - /** @var Payload $payload */ - $payload = $this->interceptor->startRequest->getInput()?->getPayloads()[0] ?? null; - self::assertNotNull($payload); - - self::assertSame(EncodingKeys::METADATA_ENCODING_RAW_VALUE, $payload->getMetadata()['encoding']); - - // Check result value from interceptor - /** @var Payload $resultPayload */ - $resultPayload = $this->interceptor->result->toPayloads()->getPayloads()[0]; - self::assertSame(EncodingKeys::METADATA_ENCODING_RAW_VALUE, $resultPayload->getMetadata()['encoding']); - } - - protected function setUp(): void - { - $this->interceptor = new Interceptor(); - parent::setUp(); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_DataConverter_RawValue')] - public function run() - { - return yield new RawValue(new Payload(['data' => 'hello world'])); - } -} - -class Interceptor implements GrpcClientInterceptor, WorkflowClientCallsInterceptor -{ - use WorkflowClientCallsInterceptorTrait; - - public ?StartWorkflowExecutionRequest $startRequest = null; - public ?EncodedValues $result = null; - - public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object - { - $arg instanceof StartWorkflowExecutionRequest and $this->startRequest = $arg; - return $next($method, $arg, $ctx); - } - - public function getResult(GetResultInput $input, callable $next): ?EncodedValues - { - return $this->result = $next($input); - } -} From 51a314626397beb31f8f1cecf95eebb2f7d77617 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Fri, 9 Jan 2026 10:21:57 +0400 Subject: [PATCH 4/6] refactor: cleanup test --- tests/Acceptance/Extra/DataConverter/RawValueTest.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/Acceptance/Extra/DataConverter/RawValueTest.php b/tests/Acceptance/Extra/DataConverter/RawValueTest.php index 68e18946b..58fe2e572 100644 --- a/tests/Acceptance/Extra/DataConverter/RawValueTest.php +++ b/tests/Acceptance/Extra/DataConverter/RawValueTest.php @@ -40,14 +40,13 @@ public function run() { $rawValue = new RawValue(new Payload(['data' => 'hello world'])); - yield Workflow::newActivityStub( + $activity = Workflow::newActivityStub( RawValueActivity::class, ActivityOptions::new() - ->withStartToCloseTimeout(10), - ) - ->bypass($rawValue); + ->withStartToCloseTimeout(3), + ); - return yield $rawValue; + return yield $activity->bypass($rawValue); } } From d4e5a4f5b8529d263658f99b3ba2e9ac94617fa9 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Fri, 9 Jan 2026 11:36:35 +0400 Subject: [PATCH 5/6] fix: correct tests --- src/DataConverter/RawValueConverter.php | 8 ++------ tests/Acceptance/Extra/DataConverter/RawValueTest.php | 10 +++++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/DataConverter/RawValueConverter.php b/src/DataConverter/RawValueConverter.php index 4cc93dc4f..a3b8e9d52 100644 --- a/src/DataConverter/RawValueConverter.php +++ b/src/DataConverter/RawValueConverter.php @@ -21,7 +21,7 @@ public function getEncodingType(): string return EncodingKeys::METADATA_ENCODING_RAW_VALUE; } - public function toPayload($value): ?Payload + public function toPayload(mixed $value): ?Payload { if (!$value instanceof RawValue) { return null; @@ -35,10 +35,6 @@ public function toPayload($value): ?Payload public function fromPayload(Payload $payload, Type $type): RawValue { - if (!$type->isClass() || $type->getName() !== RawValue::class) { - throw new DataConverterException(\sprintf('Unable to convert raw data to non %s type', RawValue::class)); - } - - return new RawValue($payload); + return new RawValue(clone $payload); } } diff --git a/tests/Acceptance/Extra/DataConverter/RawValueTest.php b/tests/Acceptance/Extra/DataConverter/RawValueTest.php index 58fe2e572..8833a7d3a 100644 --- a/tests/Acceptance/Extra/DataConverter/RawValueTest.php +++ b/tests/Acceptance/Extra/DataConverter/RawValueTest.php @@ -24,7 +24,7 @@ public function check( #[Stub('Extra_DataConverter_RawValue')] WorkflowStubInterface $stub, ): void { - $result = $stub->getResult(RawValue::class); + $result = $stub->getResult(); self::assertInstanceOf(RawValue::class, $result); self::assertInstanceOf(Payload::class, $result->getPayload()); @@ -43,7 +43,7 @@ public function run() $activity = Workflow::newActivityStub( RawValueActivity::class, ActivityOptions::new() - ->withStartToCloseTimeout(3), + ->withScheduleToCloseTimeout('1 minute'), ); return yield $activity->bypass($rawValue); @@ -53,9 +53,9 @@ public function run() #[ActivityInterface(prefix: 'RawValueActivity.')] class RawValueActivity { - #[ActivityMethod('Bypass')] - public function bypass(RawValue $arg): iterable + #[ActivityMethod] + public function bypass(RawValue $arg) { - yield $arg; + return $arg; } } From dffce4715449e0ed359fb8ff1a72d5bc0a200bb6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 9 Jan 2026 07:37:12 +0000 Subject: [PATCH 6/6] style(php-cs-fixer): fix coding standards --- src/DataConverter/RawValueConverter.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/DataConverter/RawValueConverter.php b/src/DataConverter/RawValueConverter.php index a3b8e9d52..49aa1af82 100644 --- a/src/DataConverter/RawValueConverter.php +++ b/src/DataConverter/RawValueConverter.php @@ -12,7 +12,6 @@ namespace Temporal\DataConverter; use Temporal\Api\Common\V1\Payload; -use Temporal\Exception\DataConverterException; class RawValueConverter extends Converter {