From 4ce113b3824bf740795daa1510ee04b0b70ae51b Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 29 Oct 2025 10:00:29 +0400 Subject: [PATCH 1/2] feat: Add `WorkflowContextInterface::getInstance()`; Throw OutOfContextException if Workflow or Activity facades are called from a wrong context; Fix psalm issues; --- psalm-baseline.xml | 46 ++----------------- src/Activity.php | 26 ++++++----- src/Internal/Support/Facade.php | 8 ---- .../Transport/Router/InvokeActivity.php | 1 + src/Workflow.php | 22 +++++---- src/Workflow/WorkflowContextInterface.php | 6 +++ 6 files changed, 39 insertions(+), 70 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index d2e6b6a4e..1c68e7830 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -342,34 +342,11 @@ getDetails()]]> - - &RepeatedField]]> - - - &RepeatedField]]> - - - &RepeatedField]]> - - - status->getDetails()]]> - - - - - - - - - - - - @@ -827,9 +804,6 @@ - - - getDetails()]]> getSummary()]]> @@ -997,13 +971,6 @@ getName()]]> - - - |null]]> - - - - @@ -1513,16 +1480,9 @@ - - newActivityStub($class, $options)]]> - registerQuery($queryType, $handler, $description)]]> - registerSignal($name, $handler, $description)]]> - - - - - - + + + diff --git a/src/Activity.php b/src/Activity.php index fe5886e64..9b9d069d7 100644 --- a/src/Activity.php +++ b/src/Activity.php @@ -17,13 +17,25 @@ use Temporal\DataConverter\Type; use Temporal\DataConverter\ValuesInterface; use Temporal\Exception\OutOfContextException; +use Temporal\Internal\Activity\ActivityContext; use Temporal\Internal\Support\Facade; -/** - * @template-extends Facade - */ final class Activity extends Facade { + /** + * Get the current Activity context. + * @throws OutOfContextException + */ + public static function getCurrentContext(): ActivityContextInterface + { + $ctx = parent::getCurrentContext(); + /** @var ActivityContext $ctx */ + $ctx::class === ActivityContext::class or throw new OutOfContextException( + 'The Activity facade can only be used in the context of an activity execution.', + ); + return $ctx; + } + /** * Returns information about current activity execution. * @@ -31,7 +43,6 @@ final class Activity extends Facade */ public static function getInfo(): ActivityInfo { - /** @var ActivityContextInterface $context */ $context = self::getCurrentContext(); return $context->getInfo(); @@ -57,7 +68,6 @@ public static function getInfo(): ActivityInfo */ public static function getInput(): ValuesInterface { - /** @var ActivityContextInterface $context */ $context = self::getCurrentContext(); return $context->getInput(); @@ -72,7 +82,6 @@ public static function getInput(): ValuesInterface */ public static function hasHeartbeatDetails(): bool { - /** @var ActivityContextInterface $context */ $context = self::getCurrentContext(); return $context->hasHeartbeatDetails(); @@ -88,7 +97,6 @@ public static function hasHeartbeatDetails(): bool */ public static function getHeartbeatDetails($type = null): mixed { - /** @var ActivityContextInterface $context */ $context = self::getCurrentContext(); return $context->getLastHeartbeatDetails($type); @@ -101,7 +109,6 @@ public static function getHeartbeatDetails($type = null): mixed */ public static function getCancellationDetails(): ?ActivityCancellationDetails { - /** @var ActivityContextInterface $context */ $context = self::getCurrentContext(); return $context->getCancellationDetails(); @@ -118,7 +125,6 @@ public static function getCancellationDetails(): ?ActivityCancellationDetails */ public static function doNotCompleteOnReturn(): void { - /** @var ActivityContextInterface $context */ $context = self::getCurrentContext(); $context->doNotCompleteOnReturn(); @@ -150,7 +156,6 @@ public static function doNotCompleteOnReturn(): void */ public static function heartbeat($details): void { - /** @var ActivityContextInterface $context */ $context = self::getCurrentContext(); $context->heartbeat($details); @@ -161,7 +166,6 @@ public static function heartbeat($details): void */ public static function getInstance(): object { - /** @var ActivityContextInterface $context */ $context = self::getCurrentContext(); return $context->getInstance(); diff --git a/src/Internal/Support/Facade.php b/src/Internal/Support/Facade.php index dea6ef857..bf13a67eb 100644 --- a/src/Internal/Support/Facade.php +++ b/src/Internal/Support/Facade.php @@ -13,9 +13,6 @@ use Temporal\Exception\OutOfContextException; -/** - * @template T of object - */ abstract class Facade { /** @@ -26,9 +23,6 @@ abstract class Facade 'from the currently running process' ; - /** - * @var object|null - */ private static ?object $ctx = null; /** @@ -40,7 +34,6 @@ private function __construct() } /** - * @param object|null $ctx * @internal */ public static function setCurrentContext(?object $ctx): void @@ -49,7 +42,6 @@ public static function setCurrentContext(?object $ctx): void } /** - * @return object * @throws OutOfContextException */ public static function getCurrentContext(): object diff --git a/src/Internal/Transport/Router/InvokeActivity.php b/src/Internal/Transport/Router/InvokeActivity.php index 1ec3c4e8c..ffa1f537d 100644 --- a/src/Internal/Transport/Router/InvokeActivity.php +++ b/src/Internal/Transport/Router/InvokeActivity.php @@ -101,6 +101,7 @@ static function (ActivityInput $input) use ($handler, $context): mixed { 'handleActivityInbound', )(new ActivityInput($context->getInput(), $context->getHeader())); + /** @var ActivityContext $context */ $context = Activity::getCurrentContext(); if ($context->isDoNotCompleteOnReturn()) { $resolver->reject(DoNotCompleteOnResultException::create()); diff --git a/src/Workflow.php b/src/Workflow.php index 1772f59a5..427ff1ccb 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -45,18 +45,28 @@ * * This is main class you can use in your workflow code. * - * @method static ScopeContext getCurrentContext() Get current workflow context. - * * @psalm-import-type TypeEnum from Type * @psalm-import-type DateIntervalValue from DateInterval * @see DateInterval - * - * @template-extends Facade */ final class Workflow extends Facade { public const DEFAULT_VERSION = -1; + /** + * Get the current Workflow context. + * @throws OutOfContextException + */ + public static function getCurrentContext(): ScopedContextInterface + { + $ctx = parent::getCurrentContext(); + /** @var ScopeContext $ctx */ + $ctx::class === ScopeContext::class or throw new OutOfContextException( + 'The Workflow facade can be used only inside workflow code.', + ); + return $ctx; + } + /** * Returns current datetime. * @@ -995,7 +1005,6 @@ public static function getStackTrace(): string */ public static function allHandlersFinished(): bool { - /** @var ScopedContextInterface $context */ $context = self::getCurrentContext(); return $context->allHandlersFinished(); @@ -1081,7 +1090,6 @@ public static function upsertTypedSearchAttributes(SearchAttributeUpdate ...$upd */ public static function uuid(): PromiseInterface { - /** @var ScopedContextInterface $context */ $context = self::getCurrentContext(); return $context->uuid(); @@ -1094,7 +1102,6 @@ public static function uuid(): PromiseInterface */ public static function uuid4(): PromiseInterface { - /** @var ScopedContextInterface $context */ $context = self::getCurrentContext(); return $context->uuid4(); @@ -1111,7 +1118,6 @@ public static function uuid4(): PromiseInterface */ public static function uuid7(?\DateTimeInterface $dateTime = null): PromiseInterface { - /** @var ScopedContextInterface $context */ $context = self::getCurrentContext(); return $context->uuid7($dateTime); diff --git a/src/Workflow/WorkflowContextInterface.php b/src/Workflow/WorkflowContextInterface.php index e0dd1c475..85d55c141 100644 --- a/src/Workflow/WorkflowContextInterface.php +++ b/src/Workflow/WorkflowContextInterface.php @@ -19,6 +19,7 @@ use Temporal\Common\SearchAttributes\SearchAttributeUpdate; use Temporal\DataConverter\Type; use Temporal\DataConverter\ValuesInterface; +use Temporal\Exception\OutOfContextException; use Temporal\Internal\Support\DateInterval; use Temporal\Worker\Transport\Command\RequestInterface; use Temporal\Worker\Environment\EnvironmentInterface; @@ -440,4 +441,9 @@ public function uuid7(?\DateTimeInterface $dateTime = null): PromiseInterface; * Logs in replay mode are omitted unless {@see WorkerOptions::$enableLoggingInReplay} is set to true. */ public function getLogger(): LoggerInterface; + + /** + * Get the currently running Workflow instance. + */ + public function getInstance(): object; } From c94a7c0af126e9096b23e8d1844c245ce4e6b086 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 29 Oct 2025 11:44:22 +0400 Subject: [PATCH 2/2] chore: Fix tests and psalm issues --- .../Transport/Router/StartWorkflow.php | 2 -- src/Internal/Workflow/ActivityStub.php | 5 +--- src/Internal/Workflow/ChildWorkflowStub.php | 5 +--- .../Workflow/ExternalWorkflowStub.php | 1 - src/Workflow.php | 26 ++++++++++++------- src/Workflow/WorkflowContextInterface.php | 1 - 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/Internal/Transport/Router/StartWorkflow.php b/src/Internal/Transport/Router/StartWorkflow.php index d0b9dd506..97edf8b25 100644 --- a/src/Internal/Transport/Router/StartWorkflow.php +++ b/src/Internal/Transport/Router/StartWorkflow.php @@ -25,7 +25,6 @@ use Temporal\Internal\Workflow\WorkflowContext; use Temporal\Worker\FeatureFlags; use Temporal\Worker\Transport\Command\ServerRequestInterface; -use Temporal\Workflow; use Temporal\Workflow\WorkflowInfo; final class StartWorkflow extends Route @@ -90,7 +89,6 @@ public function handle(ServerRequestInterface $request, array $headers, Deferred ); $runId = $request->getID(); - Workflow::setCurrentContext($context); $process = new Process($this->services, $runId, $instance); $this->services->running->add($process); $resolver->resolve(EncodedValues::fromValues([null])); diff --git a/src/Internal/Workflow/ActivityStub.php b/src/Internal/Workflow/ActivityStub.php index 980455f03..4ecc930a1 100644 --- a/src/Internal/Workflow/ActivityStub.php +++ b/src/Internal/Workflow/ActivityStub.php @@ -70,9 +70,6 @@ public function execute( protected function request(RequestInterface $request): PromiseInterface { - /** @var Workflow\WorkflowContextInterface $context */ - $context = Workflow::getCurrentContext(); - - return $context->request($request); + return Workflow::getCurrentContext()->request($request); } } diff --git a/src/Internal/Workflow/ChildWorkflowStub.php b/src/Internal/Workflow/ChildWorkflowStub.php index 2aa21b068..2bc9fb5b4 100644 --- a/src/Internal/Workflow/ChildWorkflowStub.php +++ b/src/Internal/Workflow/ChildWorkflowStub.php @@ -125,10 +125,7 @@ function (WorkflowExecution $execution) use ($name, $args) { protected function request(RequestInterface $request, bool $cancellable = true): PromiseInterface { - /** @var Workflow\WorkflowContextInterface $context */ - $context = Workflow::getCurrentContext(); - - return $context->request($request, cancellable: $cancellable); + return Workflow::getCurrentContext()->request($request, cancellable: $cancellable); } private function getOptionArray(): array diff --git a/src/Internal/Workflow/ExternalWorkflowStub.php b/src/Internal/Workflow/ExternalWorkflowStub.php index 9f047fb7f..75b4ff374 100644 --- a/src/Internal/Workflow/ExternalWorkflowStub.php +++ b/src/Internal/Workflow/ExternalWorkflowStub.php @@ -77,7 +77,6 @@ public function cancel(): PromiseInterface private function request(RequestInterface $request): PromiseInterface { // todo intercept - /** @var Workflow\WorkflowContextInterface $context */ $context = Workflow::getCurrentContext(); return $context->request($request); diff --git a/src/Workflow.php b/src/Workflow.php index 427ff1ccb..06bbb4beb 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -23,7 +23,6 @@ use Temporal\Exception\Failure\CanceledFailure; use Temporal\Exception\OutOfContextException; use Temporal\Internal\Support\Facade; -use Temporal\Internal\Workflow\ScopeContext; use Temporal\Workflow\ActivityStubInterface; use Temporal\Workflow\CancellationScopeInterface; use Temporal\Workflow\ChildWorkflowOptions; @@ -57,11 +56,10 @@ final class Workflow extends Facade * Get the current Workflow context. * @throws OutOfContextException */ - public static function getCurrentContext(): ScopedContextInterface + public static function getCurrentContext(): WorkflowContextInterface { $ctx = parent::getCurrentContext(); - /** @var ScopeContext $ctx */ - $ctx::class === ScopeContext::class or throw new OutOfContextException( + $ctx instanceof WorkflowContextInterface or throw new OutOfContextException( 'The Workflow facade can be used only inside workflow code.', ); return $ctx; @@ -202,7 +200,9 @@ public static function getInput(): ValuesInterface */ public static function async(callable $task): CancellationScopeInterface { - return self::getCurrentContext()->async($task); + $ctx = self::getCurrentContext(); + \assert($ctx instanceof ScopedContextInterface); + return $ctx->async($task); } /** @@ -254,7 +254,9 @@ public static function async(callable $task): CancellationScopeInterface */ public static function asyncDetached(callable $task): CancellationScopeInterface { - return self::getCurrentContext()->asyncDetached($task); + $ctx = self::getCurrentContext(); + \assert($ctx instanceof ScopedContextInterface); + return $ctx->asyncDetached($task); } /** @@ -362,7 +364,9 @@ public static function registerQuery( callable $handler, string $description = '', ): ScopedContextInterface { - return self::getCurrentContext()->registerQuery($queryType, $handler, $description); + $ctx = self::getCurrentContext(); + \assert($ctx instanceof ScopedContextInterface); + return $ctx->registerQuery($queryType, $handler, $description); } /** @@ -382,7 +386,9 @@ public static function registerQuery( */ public static function registerSignal(string $name, callable $handler, string $description = ''): ScopedContextInterface { - return self::getCurrentContext()->registerSignal($name, $handler, $description); + $ctx = self::getCurrentContext(); + \assert($ctx instanceof ScopedContextInterface); + return $ctx->registerSignal($name, $handler, $description); } /** @@ -501,7 +507,9 @@ public static function registerUpdate( ?callable $validator = null, string $description = '', ): ScopedContextInterface { - return self::getCurrentContext()->registerUpdate($name, $handler, $validator, $description); + $ctx = self::getCurrentContext(); + \assert($ctx instanceof ScopedContextInterface); + return $ctx->registerUpdate($name, $handler, $validator, $description); } /** diff --git a/src/Workflow/WorkflowContextInterface.php b/src/Workflow/WorkflowContextInterface.php index 85d55c141..945eda1cd 100644 --- a/src/Workflow/WorkflowContextInterface.php +++ b/src/Workflow/WorkflowContextInterface.php @@ -19,7 +19,6 @@ use Temporal\Common\SearchAttributes\SearchAttributeUpdate; use Temporal\DataConverter\Type; use Temporal\DataConverter\ValuesInterface; -use Temporal\Exception\OutOfContextException; use Temporal\Internal\Support\DateInterval; use Temporal\Worker\Transport\Command\RequestInterface; use Temporal\Worker\Environment\EnvironmentInterface;