Skip to content
Open
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
11 changes: 11 additions & 0 deletions src/Interceptor/Trait/WorkflowInboundCallsInterceptorTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Temporal\Interceptor\Trait;

use Temporal\Interceptor\WorkflowInbound\InitInput;
use Temporal\Interceptor\WorkflowInbound\QueryInput;
use Temporal\Interceptor\WorkflowInbound\SignalInput;
use Temporal\Interceptor\WorkflowInbound\UpdateInput;
Expand All @@ -24,6 +25,16 @@
*/
trait WorkflowInboundCallsInterceptorTrait
{
/**
* Default implementation of the `init` method.
*
* @see WorkflowInboundCallsInterceptor::init()
*/
public function init(InitInput $input, callable $next): void
{
$next($input);
}

/**
* Default implementation of the `execute` method.
*
Expand Down
40 changes: 40 additions & 0 deletions src/Interceptor/WorkflowInbound/InitInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Temporal\Interceptor\WorkflowInbound;

use Temporal\Interceptor\HeaderInterface;
use Temporal\Workflow\WorkflowInfo;

/**
* @psalm-immutable
*/
class InitInput
{
/**
* @no-named-arguments
* @internal Don't use the constructor. Use {@see self::with()} instead.
*/
public function __construct(
public readonly WorkflowInfo $info,
public readonly HeaderInterface $header,
) {}

public function with(
?WorkflowInfo $info = null,
?HeaderInterface $header = null,
): self {
return new self(
$info ?? $this->info,
$header ?? $this->header,
);
}
}
9 changes: 9 additions & 0 deletions src/Interceptor/WorkflowInboundCallsInterceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Temporal\Interceptor;

use Temporal\Interceptor\Trait\WorkflowInboundCallsInterceptorTrait;
use Temporal\Interceptor\WorkflowInbound\InitInput;
use Temporal\Interceptor\WorkflowInbound\QueryInput;
use Temporal\Interceptor\WorkflowInbound\SignalInput;
use Temporal\Interceptor\WorkflowInbound\UpdateInput;
Expand Down Expand Up @@ -40,6 +41,14 @@
*/
interface WorkflowInboundCallsInterceptor extends Interceptor
{
/**
* Called when workflow instance is initialized, before {@see execute()}.
* Allows interceptors to perform per-workflow-execution setup.
*
* @param callable(InitInput): void $next
*/
public function init(InitInput $input, callable $next): void;

/**
* @param callable(WorkflowInput): void $next
*/
Expand Down
17 changes: 17 additions & 0 deletions src/Internal/Workflow/Process/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Temporal\DataConverter\ValuesInterface;
use Temporal\Exception\DestructMemorizedInstanceException;
use Temporal\Exception\Failure\CanceledFailure;
use Temporal\Interceptor\WorkflowInbound\InitInput;
use Temporal\Interceptor\WorkflowInbound\QueryInput;
use Temporal\Interceptor\WorkflowInbound\SignalInput;
use Temporal\Interceptor\WorkflowInbound\UpdateInput;
Expand Down Expand Up @@ -214,6 +215,22 @@ public function initAndStart(

$context->setReadonly(false);

// Init interceptors
//
// Notify interceptors that a workflow instance has been initialized
$this->services->interceptorProvider
->getPipeline(WorkflowInboundCallsInterceptor::class)
->with(
static function (InitInput $_input): void {
// no-op: interceptors have been notified
},
/** @see WorkflowInboundCallsInterceptor::init() */
'init',
)(new InitInput(
$context->getInfo(),
$context->getHeader(),
));

// Execute
//
// Run workflow handler in an interceptor pipeline
Expand Down
114 changes: 114 additions & 0 deletions tests/Acceptance/Extra/Interceptors/InitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace Temporal\Tests\Acceptance\Extra\Interceptors\Init;

use PHPUnit\Framework\Attributes\Test;
use Temporal\Client\WorkflowStubInterface;
use Temporal\Exception\Client\WorkflowFailedException;
use Temporal\Exception\Failure\ApplicationFailure;
use Temporal\Interceptor\PipelineProvider;
use Temporal\Interceptor\SimplePipelineProvider;
use Temporal\Interceptor\Trait\WorkflowInboundCallsInterceptorTrait;
use Temporal\Interceptor\WorkflowInbound\InitInput;
use Temporal\Interceptor\WorkflowInboundCallsInterceptor;
use Temporal\Tests\Acceptance\App\Attribute\Stub;
use Temporal\Tests\Acceptance\App\Attribute\Worker;
use Temporal\Tests\Acceptance\App\TestCase;
use Temporal\Workflow;
use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;

#[Worker(pipelineProvider: [WorkerServices::class, 'interceptors'])]
class InitTest extends TestCase
{
#[Test]
public function initIsCalledBeforeExecute(
#[Stub('Extra_Interceptors_Init')] WorkflowStubInterface $stub,
): void {
$result = $stub->getResult('array');

self::assertTrue($result['initCalled'], 'init() interceptor was not called');
self::assertSame(1, $result['initCount'], 'init() interceptor was called more than once per workflow instance');
self::assertSame('Extra_Interceptors_Init', $result['initType'], 'InitInput did not carry the correct workflow type');
self::assertTrue($result['staticContextAvailable'], 'Workflow static context was not available in init()');
}

#[Test]
public function failInInit(
#[Stub('Extra_Interceptors_Init_Failing')] WorkflowStubInterface $stub,
): void {
try {
$stub->getResult('array');
$this->fail('An exception should have been thrown.');
} catch (WorkflowFailedException $e) {
$prev = $e->getPrevious();
self::assertInstanceOf(ApplicationFailure::class, $prev);
self::assertStringContainsString('exception-in-init', $prev->getOriginalMessage());
}
}
}

class WorkerServices
{
public static function interceptors(): PipelineProvider
{
return new SimplePipelineProvider([
new WorkflowInboundInterceptor(),
]);
}
}

#[WorkflowInterface]
class TestWorkflow
{
public bool $initCalled = false;
public int $initCount = 0;
public string $initType = '';
public bool $staticContextAvailable = false;

#[WorkflowMethod(name: 'Extra_Interceptors_Init')]
public function handle(): array
{
return [
'initCalled' => $this->initCalled,
'initCount' => $this->initCount,
'initType' => $this->initType,
'staticContextAvailable' => $this->staticContextAvailable,
];
}
}

#[WorkflowInterface]
class TestFailingInInitWorkflow
{
#[WorkflowMethod(name: 'Extra_Interceptors_Init_Failing')]
public function handle(): array
{
return [];
}
}

final class WorkflowInboundInterceptor implements WorkflowInboundCallsInterceptor
{
use WorkflowInboundCallsInterceptorTrait;

public function init(InitInput $input, callable $next): void
{
if ($input->info->type->name === 'Extra_Interceptors_Init_Failing') {
throw new ApplicationFailure('exception-in-init', 'error', true);
}

$instance = Workflow::getInstance();
if ($instance instanceof TestWorkflow) {
$instance->initCalled = true;
++$instance->initCount;
$instance->initType = $input->info->type->name;
// Verify that Workflow:: static context is available in init()
$instance->staticContextAvailable = Workflow::getInfo()->type->name === $input->info->type->name;
}

$next($input);
}
}
6 changes: 6 additions & 0 deletions tests/Fixtures/src/Interceptor/InterceptorCallsCounter.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Temporal\Interceptor\WorkflowClient\UpdateWithStartInput;
use Temporal\Interceptor\WorkflowClient\UpdateWithStartOutput;
use Temporal\Interceptor\WorkflowClientCallsInterceptor;
use Temporal\Interceptor\WorkflowInbound\InitInput;
use Temporal\Interceptor\WorkflowInbound\SignalInput;
use Temporal\Interceptor\WorkflowInbound\UpdateInput;
use Temporal\Interceptor\WorkflowInbound\WorkflowInput;
Expand Down Expand Up @@ -70,6 +71,11 @@ public function handleActivityInbound(ActivityInput $input, callable $next): mix
return $next($input->with(header: $this->increment($input->header, __FUNCTION__)));
}

public function init(InitInput $input, callable $next): void
{
$next($input->with(header: $this->increment($input->header, __FUNCTION__)));
}

public function execute(WorkflowInput $input, callable $next): void
{
$next($input->with(header: $this->increment($input->header, __FUNCTION__)));
Expand Down
69 changes: 69 additions & 0 deletions tests/Unit/Interceptor/InitInputTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Temporal\Tests\Unit\Interceptor;

use Temporal\Interceptor\Header;
use Temporal\Interceptor\WorkflowInbound\InitInput;
use Temporal\Tests\Unit\AbstractUnit;
use Temporal\Workflow\WorkflowInfo;

/**
* @group unit
* @group interceptor
*/
class InitInputTestCase extends AbstractUnit
{
public function testConstructor(): void
{
$info = new WorkflowInfo();
$header = Header::empty();

$input = new InitInput($info, $header);

self::assertSame($info, $input->info);
self::assertSame($header, $input->header);
}

public function testWithReturnsNewInstance(): void
{
$info = new WorkflowInfo();
$header = Header::empty();
$input = new InitInput($info, $header);

$newInfo = new WorkflowInfo();
$newInput = $input->with(info: $newInfo);

self::assertNotSame($input, $newInput);
self::assertSame($newInfo, $newInput->info);
self::assertSame($header, $newInput->header);
}

public function testWithHeader(): void
{
$info = new WorkflowInfo();
$header = Header::empty();
$input = new InitInput($info, $header);

$newHeader = Header::empty();
$newInput = $input->with(header: $newHeader);

self::assertNotSame($input, $newInput);
self::assertSame($info, $newInput->info);
self::assertSame($newHeader, $newInput->header);
}

public function testWithPreservesOriginalWhenNullArgs(): void
{
$info = new WorkflowInfo();
$header = Header::empty();
$input = new InitInput($info, $header);

$newInput = $input->with();

self::assertNotSame($input, $newInput);
self::assertSame($info, $newInput->info);
self::assertSame($header, $newInput->header);
}
}
Loading
Loading