Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ $response = $operatorClient->DeleteNamespace(
> that change the behavior of the entire PHP worker.
> It's recommended to disable deprecated behavior.

#### Update headers

Client interceptors can supply update headers with `Temporal\Interceptor\Header::fromValues()`.
For updates and the update portion of `updateWithStart()`, PHP header values use the client's
configured data converter unless the header already has an explicitly supplied converter.
Prebuilt `Payload` entries supplied through `Header::fromPayloadCollection()` or `withValue()`
are forwarded unchanged, even when the same header also contains PHP values. Prepare those
payloads with any application-specific encoding they require; they are not decoded or re-encoded.

## Testing

The PHP SDK includes a toolkit for testing Workflows.
Expand Down
6 changes: 0 additions & 6 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,6 @@
<code><![CDATA[getStartWorkflow]]></code>
<code><![CDATA[getUpdateWorkflow]]></code>
</PossiblyNullReference>
<UndefinedInterfaceMethod>
<code><![CDATA[toHeader]]></code>
</UndefinedInterfaceMethod>
</file>
<file src="src/Internal/Client/WorkflowStub.php">
<ArgumentTypeCoercion>
Expand Down Expand Up @@ -411,9 +408,6 @@
<code><![CDATA[hasResult]]></code>
<code><![CDATA[toProtoWorkflowExecution]]></code>
</PossiblyNullReference>
<UndefinedInterfaceMethod>
<code><![CDATA[toHeader]]></code>
</UndefinedInterfaceMethod>
</file>
<file src="src/Internal/Declaration/Dispatcher/AutowiredPayloads.php">
<PossiblyInvalidArgument>
Expand Down
7 changes: 6 additions & 1 deletion src/Internal/Client/WorkflowStarter.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,12 @@ function (UpdateWithStartInput $input): UpdateWithStartOutput {
$i = new \Temporal\Api\Update\V1\Input();
$i->setName($input->updateInput->updateName);
$input->updateInput->arguments->isEmpty() or $i->setArgs($input->updateInput->arguments->toPayloads());
$input->updateInput->header->isEmpty() or $i->setHeader($input->updateInput->header->toHeader());
$header = $input->updateInput->header;
\assert($header instanceof Header);
if ($header->getDataConverter() === null) {
$header->setDataConverter($this->converter);
}
$header->isEmpty() or $i->setHeader($header->toHeader());
$r->setInput($i);

$ops = [
Expand Down
7 changes: 6 additions & 1 deletion src/Internal/Client/WorkflowStub.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,12 @@ static function (
$input->arguments->setDataConverter($converter);
$input->arguments->setSerializationContext($context);
$input->arguments->isEmpty() or $i->setArgs($input->arguments->toPayloads());
$input->header->isEmpty() or $i->setHeader($input->header->toHeader());
$header = $input->header;
\assert($header instanceof Header);
if ($header->getDataConverter() === null) {
$header->setDataConverter($converter);
}
$header->isEmpty() or $i->setHeader($header->toHeader());
$r->setInput($i);

try {
Expand Down
118 changes: 118 additions & 0 deletions tests/Unit/Client/UpdateHeaderTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace Temporal\Tests\Unit\Client;

use PHPUnit\Framework\Attributes\DataProvider;
use Temporal\Api\Common\V1\Payload;
use Temporal\Api\Update\V1\UpdateRef;
use Temporal\Api\Workflowservice\V1\ExecuteMultiOperationRequest;
use Temporal\Api\Workflowservice\V1\ExecuteMultiOperationResponse;
use Temporal\Api\Workflowservice\V1\ExecuteMultiOperationResponse\Response;
use Temporal\Api\Workflowservice\V1\StartWorkflowExecutionResponse;
use Temporal\Api\Workflowservice\V1\UpdateWorkflowExecutionRequest;
use Temporal\Api\Workflowservice\V1\UpdateWorkflowExecutionResponse;
use Temporal\Client\GRPC\Context;
use Temporal\Client\GRPC\ServiceClientInterface;
use Temporal\Client\WorkflowClient;
use Temporal\DataConverter\DataConverterInterface;
use Temporal\Interceptor\Header;
use Temporal\Interceptor\SimplePipelineProvider;
use Temporal\Interceptor\WorkflowClient\UpdateInput;
use Temporal\Interceptor\WorkflowClient\UpdateWithStartInput;
use Temporal\Interceptor\WorkflowClientCallsInterceptor;
use Temporal\Tests\Unit\AbstractUnit;

final class UpdateHeaderTestCase extends AbstractUnit
{
/**
* @return iterable<string, array{bool, bool}>
*/
public static function headers(): iterable
{
yield 'update, client converter' => [false, false];
yield 'update with start, client converter' => [true, false];
yield 'update, explicit converter' => [false, true];
yield 'update with start, explicit converter' => [true, true];
}

#[DataProvider('headers')]
public function testHeaderConversion(bool $withStart, bool $explicitConverter): void
{
$converter = $this->createMock(DataConverterInterface::class);
$headerConverter = $explicitConverter ? $this->createMock(DataConverterInterface::class) : $converter;
$encoded = new Payload(['metadata' => ['encoding' => 'custom/header'], 'data' => 'converted']);
$raw = new Payload(['metadata' => ['encoding' => 'custom/raw'], 'data' => "\x00raw\xff"]);
$rawBytes = $raw->serializeToString();

$headerConverter->expects(self::once())->method('toPayload')->with('value')->willReturn($encoded);
$headerConverter->expects(self::never())->method('fromPayload');
if ($explicitConverter) {
$converter->expects(self::never())->method('toPayload');
$converter->expects(self::never())->method('fromPayload');
}
$header = $explicitConverter
? Header::fromPayloadCollection(['raw' => $raw], $headerConverter)->withValue('value', 'value')
: Header::fromValues(['value' => 'value'])->withValue('raw', $raw);

$interceptor = $this->createMock(WorkflowClientCallsInterceptor::class);
$interceptor->method('update')->willReturnCallback(
static fn(UpdateInput $input, callable $next) => $next($input->with(header: $header)),
);
$interceptor->method('updateWithStart')->willReturnCallback(
static fn(UpdateWithStartInput $input, callable $next) => $next(
$input->with(updateInput: $input->updateInput->with(header: $header)),
),
);

$service = $this->createMock(ServiceClientInterface::class);
$service->method('getContext')->willReturn(Context::default());
$service->method('withContext')->willReturnSelf();
$response = (new UpdateWorkflowExecutionResponse())->setUpdateRef(
(new UpdateRef())->setUpdateId('update-id'),
);
$checkHeader = static function (UpdateWorkflowExecutionRequest $request) use ($encoded, $rawBytes): void {
$fields = $request->getRequest()->getInput()->getHeader()->getFields();
self::assertCount(2, $fields);
self::assertSame($encoded->serializeToString(), $fields['value']->serializeToString());
self::assertSame($rawBytes, $fields['raw']->serializeToString());
};
if ($withStart) {
$service->expects(self::once())->method('ExecuteMultiOperation')->willReturnCallback(
static function (ExecuteMultiOperationRequest $request) use ($checkHeader, $response) {
$checkHeader($request->getOperations()[1]->getUpdateWorkflow());

return (new ExecuteMultiOperationResponse())->setResponses([
(new Response())->setStartWorkflow(
(new StartWorkflowExecutionResponse())->setRunId('run-id'),
),
(new Response())->setUpdateWorkflow($response),
]);
},
);
} else {
$service->expects(self::once())->method('UpdateWorkflowExecution')->willReturnCallback(
static function (UpdateWorkflowExecutionRequest $request) use ($checkHeader, $response) {
$checkHeader($request);

return $response;
},
);
}
$client = new WorkflowClient(
$service,
converter: $converter,
interceptorProvider: new SimplePipelineProvider([$interceptor]),
);

if ($withStart) {
$client->updateWithStart($client->newUntypedWorkflowStub('workflow'), 'update');
} else {
$client->newUntypedRunningWorkflowStub('workflow-id')->startUpdate('update');
}

self::assertSame($headerConverter, $header->getDataConverter());
self::assertSame($rawBytes, $raw->serializeToString());
}
}
Loading