Skip to content

Commit d95c27b

Browse files
committed
Add the ability to create untyped workflow and child workflow stubs; add tests
1 parent 3cf3f2e commit d95c27b

4 files changed

Lines changed: 128 additions & 18 deletions

File tree

composer.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@
3838
"prefer-stable": true,
3939
"require": {
4040
"php": ">=8.1",
41-
"temporal/sdk": "^2.7"
41+
"temporal/sdk": "^2.8"
4242
},
4343
"suggest": {
4444
"buggregator/trap": "For better debugging and protobuf messages dumping"
4545
},
4646
"require-dev": {
47-
"buggregator/trap": "^1.3",
47+
"buggregator/trap": "^1.4",
4848
"dereuromark/composer-prefer-lowest": "^0.1.10",
49-
"phpunit/phpunit": "^10.4",
50-
"vimeo/psalm": "^5.18"
49+
"phpunit/phpunit": "^10.5",
50+
"vimeo/psalm": "^5.23"
5151
}
5252
}

src/Factory/WorkflowStub.php

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use DateInterval;
88
use Temporal\Client\WorkflowClientInterface;
99
use Temporal\Client\WorkflowOptions;
10+
use Temporal\Client\WorkflowStubInterface;
1011
use Temporal\Common\IdReusePolicy;
1112
use Temporal\Internal\Client\WorkflowProxy;
1213
use Temporal\Internal\Workflow\ChildWorkflowProxy;
@@ -18,6 +19,7 @@
1819
use Temporal\Support\Internal\RetryOptions;
1920
use Temporal\Workflow;
2021
use Temporal\Workflow\ChildWorkflowCancellationType as ChildCancelType;
22+
use Temporal\Workflow\ChildWorkflowStubInterface;
2123
use Temporal\Workflow\ParentClosePolicy;
2224
use Throwable;
2325

@@ -28,7 +30,7 @@ final class WorkflowStub
2830
*
2931
* @template T of object
3032
*
31-
* @param class-string<T> $workflow
33+
* @param class-string<T>|non-empty-string $type Workflow name or class name.
3234
* @param non-empty-string|null $taskQueue
3335
* @param int<0, max>|null $retryAttempts Maximum number of attempts. When exceeded the retries stop even
3436
* if not expired yet. If not set or set to 0, it means unlimited, and rely on activity
@@ -73,11 +75,11 @@ final class WorkflowStub
7375
* of list workflow.
7476
* @param list<mixed> $memo Specifies additional non-indexed information in result of list workflow.
7577
*
76-
* @return T|WorkflowProxy
78+
* @return ($type is class-string ? T|WorkflowProxy : WorkflowStubInterface)
7779
*/
7880
public static function workflow(
7981
WorkflowClientInterface $workflowClient,
80-
string $workflow,
82+
string $type,
8183
?string $taskQueue = null,
8284
?int $retryAttempts = null,
8385
\DateInterval|string|int|null $retryInitInterval = null,
@@ -95,7 +97,8 @@ public static function workflow(
9597
array $searchAttributes = [],
9698
array $memo = [],
9799
): object {
98-
$attributes = self::readAttributes($workflow);
100+
$isUntyped = !\class_exists($type) && !\interface_exists($type);
101+
$attributes = $isUntyped ? new AttributeCollection([]) : self::readAttributes($type);
99102

100103
// Retry options
101104
$retryOptions = RetryOptions::create(
@@ -109,6 +112,7 @@ public static function workflow(
109112

110113
$options = WorkflowOptions::new()->withRetryOptions($retryOptions);
111114
$taskQueue ??= $attributes->first(TaskQueue::class)?->name;
115+
$taskQueue === null or $options = $options->withTaskQueue($taskQueue);
112116
// Start options
113117
$startDelay === 0 or $options = $options->withWorkflowStartDelay($startDelay);
114118
$eagerStart and $options = $options->withEagerStart(true);
@@ -125,15 +129,17 @@ public static function workflow(
125129
$searchAttributes === [] or $options = $options->withSearchAttributes($searchAttributes);
126130
$memo === [] or $options = $options->withMemo($memo);
127131

128-
return $workflowClient->newWorkflowStub($workflow, $options);
132+
return $isUntyped
133+
? $workflowClient->newUntypedWorkflowStub($type, $options)
134+
: $workflowClient->newWorkflowStub($type, $options);
129135
}
130136

131137
/**
132138
* Note: must be used in Workflow context only.
133139
*
134140
* @template T of object
135141
*
136-
* @param class-string<T> $workflow
142+
* @param class-string<T>|non-empty-string $type Workflow name or class name.
137143
* @param non-empty-string|null $taskQueue Task queue to use for workflow tasks. It should match a task queue
138144
* specified when creating a {@see Worker} that hosts the workflow code.
139145
* @param non-empty-string|null $namespace Specify namespace in which workflow should be started.
@@ -174,10 +180,10 @@ public static function workflow(
174180
* of list workflow.
175181
* @param list<mixed> $memo Specifies additional non-indexed information in result of list workflow.
176182
*
177-
* @return T|ChildWorkflowProxy
183+
* @return ($type is class-string ? T|ChildWorkflowProxy : ChildWorkflowStubInterface)
178184
*/
179185
public static function childWorkflow(
180-
string $workflow,
186+
string $type,
181187
?string $taskQueue = null,
182188
?string $namespace = null,
183189
?int $retryAttempts = null,
@@ -196,7 +202,8 @@ public static function childWorkflow(
196202
array $searchAttributes = [],
197203
array $memo = [],
198204
): object {
199-
$attributes = self::readAttributes($workflow);
205+
$isUntyped = !\class_exists($type) && !\interface_exists($type);
206+
$attributes = $isUntyped ? new AttributeCollection([]) : self::readAttributes($type);
200207

201208
// Retry options
202209
$retryOptions = RetryOptions::create(
@@ -232,7 +239,9 @@ public static function childWorkflow(
232239
$searchAttributes === [] or $options = $options->withSearchAttributes($searchAttributes);
233240
$memo === [] or $options = $options->withMemo($memo);
234241

235-
return Workflow::newChildWorkflowStub($workflow, $options);
242+
return $isUntyped
243+
? Workflow::newUntypedChildWorkflowStub($type, $options)
244+
: Workflow::newChildWorkflowStub($type, $options);
236245
}
237246

238247
/**

tests/Unit/Factory/ChildWorkflowStubTest.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Temporal\Support\Factory\WorkflowStub;
1212
use Temporal\Support\Tests\Stub\Workflow\AttributedWithoutInterface;
1313
use Temporal\Workflow;
14+
use Temporal\Workflow\ChildWorkflowStubInterface;
1415

1516
final class ChildWorkflowStubTest extends TestCase
1617
{
@@ -28,11 +29,11 @@ protected function setUp(): void
2829
parent::setUp();
2930
}
3031

31-
public function testDefaultsFromAttributes()
32+
public function testDefaultsFromAttributes(): void
3233
{
3334
/** @var WorkflowOptions $options */
3435
$options = WorkflowStub::childWorkflow(
35-
workflow: AttributedWithoutInterface::class,
36+
type: AttributedWithoutInterface::class,
3637
)->options;
3738

3839
$this->assertSame('test-queue', $options->taskQueue);
@@ -43,11 +44,11 @@ public function testDefaultsFromAttributes()
4344
$this->assertSame('500.0', $options->retryOptions->maximumInterval->format('%s.%f'));
4445
}
4546

46-
public function testAttributeOverrides()
47+
public function testAttributeOverrides(): void
4748
{
4849
/** @var WorkflowOptions $options */
4950
$options = WorkflowStub::childWorkflow(
50-
workflow: AttributedWithoutInterface::class,
51+
type: AttributedWithoutInterface::class,
5152
taskQueue: 'test-queue-override',
5253
retryAttempts: 0,
5354
retryInitInterval: 10,
@@ -66,4 +67,11 @@ public function testAttributeOverrides()
6667
$this->assertSame('10.0', $options->retryOptions->initialInterval->format('%s.%f'));
6768
$this->assertSame('200.0', $options->retryOptions->maximumInterval->format('%s.%f'));
6869
}
70+
71+
public function testUntypedChildCreated(): void
72+
{
73+
$wf = WorkflowStub::childWorkflow(type: 'foo-bar');
74+
75+
self::assertInstanceOf(ChildWorkflowStubInterface::class, $wf);
76+
}
6977
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Factory;
6+
7+
use LogicException;
8+
use PHPUnit\Framework\TestCase;
9+
use RuntimeException;
10+
use Temporal\Client\WorkflowClientInterface;
11+
use Temporal\Client\WorkflowOptions;
12+
use Temporal\Client\WorkflowStubInterface;
13+
use Temporal\Support\Factory\WorkflowStub;
14+
use Temporal\Support\Tests\Stub\Workflow\AttributedWithoutInterface;
15+
16+
final class WorkflowStubTest extends TestCase
17+
{
18+
public function testDefaultsFromAttributes(): void
19+
{
20+
$input = new class {
21+
public string $type;
22+
public WorkflowOptions $options;
23+
};
24+
25+
$clientMock = self::createMock(WorkflowClientInterface::class);
26+
$clientMock->method('newWorkflowStub')
27+
->willReturnCallback(function (string $class, WorkflowOptions $options) use ($input) {
28+
$input->type = $class;
29+
$input->options = $options;
30+
return $input;
31+
});
32+
WorkflowStub::workflow(
33+
$clientMock,
34+
type: AttributedWithoutInterface::class,
35+
);
36+
37+
self::assertSame(AttributedWithoutInterface::class, $input->type);
38+
self::assertSame('test-queue', $input->options->taskQueue);
39+
self::assertSame(3, $input->options->retryOptions->maximumAttempts);
40+
self::assertSame(10.0, $input->options->retryOptions->backoffCoefficient);
41+
self::assertEquals(
42+
[RuntimeException::class],
43+
$input->options->retryOptions->nonRetryableExceptions,
44+
);
45+
self::assertSame('5.0', $input->options->retryOptions->initialInterval->format('%s.%f'));
46+
self::assertSame('500.0', $input->options->retryOptions->maximumInterval->format('%s.%f'));
47+
}
48+
49+
public function testAttributeOverrides(): void
50+
{
51+
$input = new class {
52+
public string $type;
53+
public WorkflowOptions $options;
54+
};
55+
56+
$clientMock = self::createMock(WorkflowClientInterface::class);
57+
$clientMock->method('newWorkflowStub')
58+
->willReturnCallback(function (string $class, WorkflowOptions $options) use ($input) {
59+
$input->type = $class;
60+
$input->options = $options;
61+
return $input;
62+
});
63+
WorkflowStub::workflow(
64+
$clientMock,
65+
type: AttributedWithoutInterface::class,
66+
taskQueue: 'test-queue-override',
67+
retryAttempts: 0,
68+
retryInitInterval: 10,
69+
retryMaxInterval: 200,
70+
retryBackoff: 5.0,
71+
nonRetryables: [LogicException::class],
72+
);
73+
74+
self::assertSame(AttributedWithoutInterface::class, $input->type);
75+
self::assertSame('test-queue-override', $input->options->taskQueue);
76+
self::assertSame(0, $input->options->retryOptions->maximumAttempts);
77+
self::assertSame(5.0, $input->options->retryOptions->backoffCoefficient);
78+
self::assertEquals(
79+
[LogicException::class, RuntimeException::class],
80+
$input->options->retryOptions->nonRetryableExceptions,
81+
);
82+
self::assertSame('10.0', $input->options->retryOptions->initialInterval->format('%s.%f'));
83+
self::assertSame('200.0', $input->options->retryOptions->maximumInterval->format('%s.%f'));
84+
}
85+
86+
public function testUntypedWorkflowCreated(): void
87+
{
88+
$clientMock = self::createMock(WorkflowClientInterface::class);
89+
$wf = WorkflowStub::workflow($clientMock, 'foo-bar');
90+
91+
self::assertInstanceOf(WorkflowStubInterface::class, $wf);
92+
}
93+
}

0 commit comments

Comments
 (0)