From 8441af75273d1d009889a243b5252bfdcd47566f Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 14 Mar 2026 12:14:07 +0400 Subject: [PATCH 1/5] feat: improve static analysis, part 1 --- psalm-baseline.xml | 167 ------------------ src/Activity.php | 5 +- src/Activity/ActivityContextInterface.php | 5 +- src/Activity/ActivityInterface.php | 2 +- src/Activity/ActivityMethod.php | 2 +- src/Activity/ActivityOptions.php | 2 +- src/Activity/LocalActivityInterface.php | 2 +- src/Activity/LocalActivityOptions.php | 2 +- src/Client/Common/Paginator.php | 11 +- src/Common/RetryOptions.php | 2 - src/DataConverter/DataConverter.php | 5 +- src/DataConverter/DataConverterInterface.php | 6 +- src/DataConverter/EncodedCollection.php | 5 +- src/DataConverter/EncodedValues.php | 16 +- src/DataConverter/JsonConverter.php | 7 +- .../PayloadConverterInterface.php | 1 + src/DataConverter/Type.php | 14 +- src/DataConverter/ValuesInterface.php | 3 + src/Internal/Events/EventEmitterInterface.php | 2 +- src/Internal/Events/EventEmitterTrait.php | 11 +- .../Events/EventListenerInterface.php | 4 +- src/Internal/Workflow/ChildWorkflowStub.php | 4 + src/Workflow/ChildWorkflowOptions.php | 2 +- src/Workflow/ChildWorkflowStubInterface.php | 8 +- src/Workflow/QueryMethod.php | 2 +- src/Workflow/ReturnType.php | 2 +- src/Workflow/Saga.php | 2 +- src/Workflow/SignalMethod.php | 2 +- src/Workflow/UpdateMethod.php | 2 +- src/Workflow/UpdateValidatorMethod.php | 2 +- src/Workflow/WorkflowInterface.php | 2 +- src/Workflow/WorkflowMethod.php | 2 +- src/Workflow/WorkflowVersioningBehavior.php | 2 +- testing/src/WorkerMock.php | 2 +- 34 files changed, 97 insertions(+), 211 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 10e3ad5fd..96f157a69 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,35 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -40,17 +10,6 @@ - - - current()]]> - - - - - - - - @@ -274,62 +233,16 @@ - - - - - - - - - - - - - - - values]]> - - - - - - - - getName()]]> - - - getCode()]]> - getCode()]]> - - - name]]> - - - - - getName()]]> - - - - - getName()]]> - - - - - - @@ -638,25 +551,6 @@ - - - - - - - - - - - - - - - - - - - last]]> @@ -1079,9 +973,6 @@ - - getReturnType()]]> - @@ -1105,9 +996,6 @@ start(...$args)->then(fn() => $this->getResult($returnType))]]> - - - @@ -1483,59 +1371,4 @@ - - - - - - - - - - - - - - - - - - - - - - - getCode()]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Activity.php b/src/Activity.php index 8bc210c8a..166313bdc 100644 --- a/src/Activity.php +++ b/src/Activity.php @@ -19,6 +19,9 @@ use Temporal\Exception\OutOfContextException; use Temporal\Internal\Support\Facade; +/** + * @psalm-import-type TType from Type + */ final class Activity extends Facade { /** @@ -92,7 +95,7 @@ public static function hasHeartbeatDetails(): bool * * This method retrieves the payload that was passed into the last call of the {@see Activity::heartbeat()} method. * - * @param Type|string|\ReflectionType|\ReflectionClass|null $type + * @psalm-param TType $type * @throws OutOfContextException in the absence of the activity execution context. */ public static function getHeartbeatDetails($type = null): mixed diff --git a/src/Activity/ActivityContextInterface.php b/src/Activity/ActivityContextInterface.php index 3ad652e51..47e413524 100644 --- a/src/Activity/ActivityContextInterface.php +++ b/src/Activity/ActivityContextInterface.php @@ -18,6 +18,9 @@ use Temporal\Exception\Client\ActivityCompletionException; use Temporal\Exception\Client\ActivityPausedException; +/** + * @psalm-import-type TType from Type + */ interface ActivityContextInterface { /** @@ -46,7 +49,7 @@ public function hasHeartbeatDetails(): bool; * * @see Activity::getHeartbeatDetails() * - * @param Type|string $type + * @psalm-param TType $type */ public function getLastHeartbeatDetails($type = null): mixed; diff --git a/src/Activity/ActivityInterface.php b/src/Activity/ActivityInterface.php index fe2fbb6c9..446bfcaa3 100644 --- a/src/Activity/ActivityInterface.php +++ b/src/Activity/ActivityInterface.php @@ -32,7 +32,7 @@ * @NamedArgumentConstructor * @Target({ "CLASS" }) */ -#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_CLASS)] class ActivityInterface { /** diff --git a/src/Activity/ActivityMethod.php b/src/Activity/ActivityMethod.php index 4942402b1..71137725b 100644 --- a/src/Activity/ActivityMethod.php +++ b/src/Activity/ActivityMethod.php @@ -20,7 +20,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_METHOD)] final class ActivityMethod { /** diff --git a/src/Activity/ActivityOptions.php b/src/Activity/ActivityOptions.php index d0548f784..3274ae2af 100644 --- a/src/Activity/ActivityOptions.php +++ b/src/Activity/ActivityOptions.php @@ -154,7 +154,7 @@ public function mergeWith(?MethodRetry $retry = null): self $self = clone $this; if ($retry !== null && $this->diff->isPresent($self, 'retryOptions')) { - $self->retryOptions = $this->retryOptions->mergeWith($retry); + $self->retryOptions = $this->retryOptions?->mergeWith($retry); } return $self; diff --git a/src/Activity/LocalActivityInterface.php b/src/Activity/LocalActivityInterface.php index 9ed095586..da6d71861 100644 --- a/src/Activity/LocalActivityInterface.php +++ b/src/Activity/LocalActivityInterface.php @@ -32,7 +32,7 @@ * @NamedArgumentConstructor * @Target({ "CLASS" }) */ -#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_CLASS)] final class LocalActivityInterface extends ActivityInterface { /** diff --git a/src/Activity/LocalActivityOptions.php b/src/Activity/LocalActivityOptions.php index 4099b1bd2..069f81a97 100644 --- a/src/Activity/LocalActivityOptions.php +++ b/src/Activity/LocalActivityOptions.php @@ -86,7 +86,7 @@ public function mergeWith(?MethodRetry $retry = null): self $self = clone $this; if ($retry !== null && $this->diff->isPresent($self, 'retryOptions')) { - $self->retryOptions = $this->retryOptions->mergeWith($retry); + $self->retryOptions = $this->retryOptions?->mergeWith($retry); } return $self; diff --git a/src/Client/Common/Paginator.php b/src/Client/Common/Paginator.php index 0c0c89a50..20485abcf 100644 --- a/src/Client/Common/Paginator.php +++ b/src/Client/Common/Paginator.php @@ -30,7 +30,16 @@ private function __construct( private readonly int $pageNumber, private ?\Closure $counter, ) { - $this->collection = $loader->current(); + $current = $loader->current(); + if (!\is_array($current)) { + throw new \InvalidArgumentException( + \sprintf( + 'Generator must return an array of items, %s returned.', + \get_debug_type($current), + ), + ); + } + $this->collection = $current; } /** diff --git a/src/Common/RetryOptions.php b/src/Common/RetryOptions.php index 83b35a874..dcbf38c91 100644 --- a/src/Common/RetryOptions.php +++ b/src/Common/RetryOptions.php @@ -130,7 +130,6 @@ public function mergeWith(?MethodRetry $retry = null): self * * @param mixed $interval parseable string, null, int|float in seconds, {@see \DateInterval}, or {@see Duration} * @return static - * @psalm-assert DateIntervalValue|null $interval */ #[Pure] public function withInitialInterval(mixed $interval): self @@ -162,7 +161,6 @@ public function withBackoffCoefficient(float $coefficient): self * * @param mixed $interval parseable string, null, int|float in seconds, {@see \DateInterval}, or {@see Duration} * @return static - * @psalm-assert DateIntervalValue|null $interval */ #[Pure] public function withMaximumInterval(mixed $interval): self diff --git a/src/DataConverter/DataConverter.php b/src/DataConverter/DataConverter.php index 2e7c87376..129876c8b 100644 --- a/src/DataConverter/DataConverter.php +++ b/src/DataConverter/DataConverter.php @@ -14,6 +14,9 @@ use Temporal\Api\Common\V1\Payload; use Temporal\Exception\DataConverterException; +/** + * @psalm-import-type TType from Type + */ final class DataConverter implements DataConverterInterface { /** @@ -40,7 +43,7 @@ public static function createDefault(): DataConverterInterface } /** - * @param string|\ReflectionClass|\ReflectionType|Type|null $type + * @param TType $type */ public function fromPayload(Payload $payload, $type): mixed { diff --git a/src/DataConverter/DataConverterInterface.php b/src/DataConverter/DataConverterInterface.php index c3d7d114c..7b8347842 100644 --- a/src/DataConverter/DataConverterInterface.php +++ b/src/DataConverter/DataConverterInterface.php @@ -14,13 +14,15 @@ use Temporal\Api\Common\V1\Payload; use Temporal\Exception\DataConverterException; +/** + * @psalm-import-type TType from Type + */ interface DataConverterInterface { /** - * @param string|\ReflectionClass|\ReflectionType|Type|null $type + * @param TType $type * @return mixed * - * @psalm-mutation-free * @throws DataConverterException */ public function fromPayload(Payload $payload, mixed $type); diff --git a/src/DataConverter/EncodedCollection.php b/src/DataConverter/EncodedCollection.php index 75ef277dd..dd6fff975 100644 --- a/src/DataConverter/EncodedCollection.php +++ b/src/DataConverter/EncodedCollection.php @@ -19,6 +19,7 @@ * @psalm-type TKey = array-key * @psalm-type TValue = mixed * @psalm-type TPayloadsCollection = \Traversable&\ArrayAccess&\Countable + * @psalm-import-type TypeEnum from Type * * @implements \IteratorAggregate */ @@ -27,7 +28,7 @@ class EncodedCollection implements \IteratorAggregate, \Countable private ?DataConverterInterface $converter = null; /** - * @var TPayloadsCollection|null + * @psalm-var TPayloadsCollection|null */ private ?\ArrayAccess $payloads = null; @@ -89,7 +90,7 @@ public function isEmpty(): bool /** * @param array-key $name - * @param Type|string|null $type + * @param Type|TypeEnum|mixed $type */ public function getValue(int|string $name, mixed $type = null): mixed { diff --git a/src/DataConverter/EncodedValues.php b/src/DataConverter/EncodedValues.php index 81e371d4e..79d681bbf 100644 --- a/src/DataConverter/EncodedValues.php +++ b/src/DataConverter/EncodedValues.php @@ -25,6 +25,8 @@ * @psalm-type TPayloadsCollection = Traversable&ArrayAccess&Countable * @psalm-type TKey = int * @psalm-type TValue = string + * @psalm-import-type TType from Type + * @final */ class EncodedValues implements ValuesInterface { @@ -47,7 +49,7 @@ private function __construct() {} public static function empty(): static { - $ev = new static(); + $ev = new self(); $ev->values = []; return $ev; @@ -74,7 +76,7 @@ public static function sliceValues( /** * Decode promise response upon returning it to the domain layer. * - * @param string|\ReflectionClass|\ReflectionType|Type|null $type + * @psalm-param TType $type */ public static function decodePromise(PromiseInterface $promise, $type = null): PromiseInterface { @@ -91,7 +93,7 @@ static function (mixed $value) use ($type) { public static function fromValues(array $values, ?DataConverterInterface $dataConverter = null): static { - $ev = new static(); + $ev = new self(); $ev->values = \array_values($values); $ev->converter = $dataConverter; @@ -105,7 +107,7 @@ public static function fromPayloadCollection( \Traversable $payloads, ?DataConverterInterface $dataConverter = null, ): static { - $ev = new static(); + $ev = new self(); $ev->payloads = $payloads; $ev->converter = $dataConverter; @@ -208,8 +210,10 @@ private function toProtoCollection(): array return $data; } - foreach ($this->values as $key => $value) { - $data[$key] = $this->valueToPayload($value); + if ($this->values !== null) { + foreach ($this->values as $key => $value) { + $data[$key] = $this->valueToPayload($value); + } } return $data; diff --git a/src/DataConverter/JsonConverter.php b/src/DataConverter/JsonConverter.php index a7d447d75..f04d7eb85 100644 --- a/src/DataConverter/JsonConverter.php +++ b/src/DataConverter/JsonConverter.php @@ -64,7 +64,7 @@ public function toPayload($value): ?Payload try { return $this->create(\json_encode($value, self::JSON_FLAGS)); } catch (\Throwable $e) { - throw new DataConverterException($e->getMessage(), $e->getCode(), $e); + throw new DataConverterException($e->getMessage(), (int) $e->getCode(), $e); } } @@ -82,7 +82,7 @@ public function fromPayload(Payload $payload, Type $type) self::JSON_FLAGS, ); } catch (\Throwable $e) { - throw new DataConverterException($e->getMessage(), $e->getCode(), $e); + throw new DataConverterException($e->getMessage(), (int) $e->getCode(), $e); } if ($data === null && $type->allowsNull()) { @@ -151,6 +151,9 @@ public function fromPayload(Payload $payload, Type $type) try { $reflection = new \ReflectionClass($type->getName()); if (PHP_VERSION_ID >= 80104 && $reflection->isEnum()) { + /** + * @var \UnitEnum $data + */ return $reflection->getConstant($data->name); } } catch (\ReflectionException $e) { diff --git a/src/DataConverter/PayloadConverterInterface.php b/src/DataConverter/PayloadConverterInterface.php index 24b125787..e0ba39463 100644 --- a/src/DataConverter/PayloadConverterInterface.php +++ b/src/DataConverter/PayloadConverterInterface.php @@ -34,6 +34,7 @@ public function toPayload($value): ?Payload; * @return mixed * * @throws DataConverterException + * @mutation-free */ public function fromPayload(Payload $payload, Type $type); } diff --git a/src/DataConverter/Type.php b/src/DataConverter/Type.php index 1ecb30567..4f605ba1c 100644 --- a/src/DataConverter/Type.php +++ b/src/DataConverter/Type.php @@ -14,7 +14,10 @@ use Temporal\Workflow\ReturnType; /** + * @template TIsClass of bool + * * @psalm-type TypeEnum = Type::TYPE_* + * @psalm-type TType = string|\ReflectionClass|\ReflectionType|Type|null */ final class Type { @@ -33,7 +36,7 @@ final class Type private readonly bool $allowsNull; /** - * @param TypeEnum|string $name + * @psalm-param TypeEnum|string $name */ public function __construct( private readonly string $name = Type::TYPE_ANY, @@ -75,7 +78,7 @@ public static function fromReflectionType(\ReflectionType $type): self } /** - * @param string|\ReflectionClass|\ReflectionType|Type|ReturnType $type + * @param string|\ReflectionClass|\ReflectionType|Type|ReturnType|null $type */ public static function create($type): Type { @@ -89,6 +92,9 @@ public static function create($type): Type }; } + /** + * @psalm-return (TIsClass is true ? class-string : string) + */ public function getName(): string { return $this->name; @@ -104,6 +110,10 @@ public function isUntyped(): bool return $this->name === self::TYPE_ANY; } + /** + * @psalm-assert-if-true self $this + * @psalm-assert-if-false self $this + */ public function isClass(): bool { return \class_exists($this->name); diff --git a/src/DataConverter/ValuesInterface.php b/src/DataConverter/ValuesInterface.php index f1d3a662e..cfe5ca91d 100644 --- a/src/DataConverter/ValuesInterface.php +++ b/src/DataConverter/ValuesInterface.php @@ -25,6 +25,9 @@ interface ValuesInterface extends \Countable */ public function isEmpty(): bool; + /** + * @return void + */ public function setDataConverter(DataConverterInterface $converter); /** diff --git a/src/Internal/Events/EventEmitterInterface.php b/src/Internal/Events/EventEmitterInterface.php index a49df23c6..114a2a361 100644 --- a/src/Internal/Events/EventEmitterInterface.php +++ b/src/Internal/Events/EventEmitterInterface.php @@ -12,7 +12,7 @@ namespace Temporal\Internal\Events; /** - * @template-covariant T of string + * @template T of string */ interface EventEmitterInterface { diff --git a/src/Internal/Events/EventEmitterTrait.php b/src/Internal/Events/EventEmitterTrait.php index c03b71657..de834576e 100644 --- a/src/Internal/Events/EventEmitterTrait.php +++ b/src/Internal/Events/EventEmitterTrait.php @@ -15,7 +15,7 @@ * @mixin EventEmitterInterface * @mixin EventListenerInterface * - * @template-covariant T of string + * @template T of string * * @template-implements EventEmitterInterface * @template-implements EventListenerInterface @@ -23,11 +23,11 @@ trait EventEmitterTrait { /** - * @var array> + * @var array> */ protected array $once = []; - public function once(string $event, callable $then): self + public function once(string $event, callable $then): static { $this->once[$event][] = $then; @@ -36,7 +36,10 @@ public function once(string $event, callable $then): self public function emit(string $event, array $arguments = []): void { - while (($this->once[$event] ?? []) !== []) { + if (!\array_key_exists($event, $this->once)) { + return; + } + while (!empty($this->once[$event])) { $callback = \array_shift($this->once[$event]); $callback(...$arguments); } diff --git a/src/Internal/Events/EventListenerInterface.php b/src/Internal/Events/EventListenerInterface.php index c9cf01516..f36999f0c 100644 --- a/src/Internal/Events/EventListenerInterface.php +++ b/src/Internal/Events/EventListenerInterface.php @@ -12,7 +12,7 @@ namespace Temporal\Internal\Events; /** - * @template-covariant T of string + * @template T of string */ interface EventListenerInterface { @@ -20,5 +20,5 @@ interface EventListenerInterface * @param T $event * @return $this */ - public function once(string $event, callable $then): self; + public function once(string $event, callable $then): static; } diff --git a/src/Internal/Workflow/ChildWorkflowStub.php b/src/Internal/Workflow/ChildWorkflowStub.php index 2bc9fb5b4..a918581f8 100644 --- a/src/Internal/Workflow/ChildWorkflowStub.php +++ b/src/Internal/Workflow/ChildWorkflowStub.php @@ -14,6 +14,7 @@ use React\Promise\Deferred; use React\Promise\PromiseInterface; use Temporal\DataConverter\EncodedValues; +use Temporal\DataConverter\Type; use Temporal\DataConverter\ValuesInterface; use Temporal\Interceptor\Header; use Temporal\Interceptor\HeaderInterface; @@ -29,6 +30,9 @@ use Temporal\Workflow\ParentClosePolicy; use Temporal\Workflow\WorkflowExecution; +/** + * @psalm-import-type TType from Type + */ final class ChildWorkflowStub implements ChildWorkflowStubInterface { private Deferred $execution; diff --git a/src/Workflow/ChildWorkflowOptions.php b/src/Workflow/ChildWorkflowOptions.php index 29173d86b..d30c21d0b 100644 --- a/src/Workflow/ChildWorkflowOptions.php +++ b/src/Workflow/ChildWorkflowOptions.php @@ -211,7 +211,7 @@ public function mergeWith(?MethodRetry $retry = null, ?CronSchedule $cron = null $self = clone $this; if ($retry !== null && $self->diff->isPresent($self, 'retryOptions')) { - $self->retryOptions = $self->retryOptions->mergeWith($retry); + $self->retryOptions = $self->retryOptions?->mergeWith($retry); } if ($cron !== null && $self->diff->isPresent($self, 'cronSchedule')) { diff --git a/src/Workflow/ChildWorkflowStubInterface.php b/src/Workflow/ChildWorkflowStubInterface.php index f02a9139d..6b208d250 100644 --- a/src/Workflow/ChildWorkflowStubInterface.php +++ b/src/Workflow/ChildWorkflowStubInterface.php @@ -15,6 +15,9 @@ use Temporal\DataConverter\Type; use Temporal\Internal\Transport\CompletableResultInterface; +/** + * @psalm-import-type TType from Type + */ interface ChildWorkflowStubInterface { /** @@ -27,7 +30,7 @@ public function getChildWorkflowType(): string; public function getOptions(): ChildWorkflowOptions; /** - * @param Type|string|\ReflectionType|\ReflectionClass|null $returnType + * @param TType $returnType * * @return CompletableResultInterface */ @@ -40,6 +43,9 @@ public function execute(array $args = [], $returnType = null): PromiseInterface; */ public function start(...$args): PromiseInterface; + /** + * @param TType $returnType + */ public function getResult($returnType = null): PromiseInterface; /** diff --git a/src/Workflow/QueryMethod.php b/src/Workflow/QueryMethod.php index e23257aaf..001efd49c 100644 --- a/src/Workflow/QueryMethod.php +++ b/src/Workflow/QueryMethod.php @@ -26,7 +26,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_METHOD)] final class QueryMethod { /** diff --git a/src/Workflow/ReturnType.php b/src/Workflow/ReturnType.php index dff3030dd..bac8b1142 100644 --- a/src/Workflow/ReturnType.php +++ b/src/Workflow/ReturnType.php @@ -20,7 +20,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_METHOD)] final class ReturnType { public const TYPE_ANY = Type::TYPE_ANY; diff --git a/src/Workflow/Saga.php b/src/Workflow/Saga.php index 6853e3ab4..edde8d070 100644 --- a/src/Workflow/Saga.php +++ b/src/Workflow/Saga.php @@ -79,7 +79,7 @@ function () { yield Workflow::asyncDetached($handler); } catch (\Throwable $e) { if ($sagaException === null) { - $sagaException = new CompensationException($e->getMessage(), $e->getCode(), $e); + $sagaException = new CompensationException($e->getMessage(), (int) $e->getCode(), $e); } if (!$this->continueWithError) { diff --git a/src/Workflow/SignalMethod.php b/src/Workflow/SignalMethod.php index 58fc7c020..9a6bb0cdf 100644 --- a/src/Workflow/SignalMethod.php +++ b/src/Workflow/SignalMethod.php @@ -23,7 +23,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_METHOD)] final class SignalMethod { /** diff --git a/src/Workflow/UpdateMethod.php b/src/Workflow/UpdateMethod.php index a5657eb17..d32b16c87 100644 --- a/src/Workflow/UpdateMethod.php +++ b/src/Workflow/UpdateMethod.php @@ -22,7 +22,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_METHOD)] final class UpdateMethod { /** diff --git a/src/Workflow/UpdateValidatorMethod.php b/src/Workflow/UpdateValidatorMethod.php index cd2c85f3d..9324753b3 100644 --- a/src/Workflow/UpdateValidatorMethod.php +++ b/src/Workflow/UpdateValidatorMethod.php @@ -23,7 +23,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_METHOD)] final class UpdateValidatorMethod { /** diff --git a/src/Workflow/WorkflowInterface.php b/src/Workflow/WorkflowInterface.php index 5f1703018..63e7cf9f3 100644 --- a/src/Workflow/WorkflowInterface.php +++ b/src/Workflow/WorkflowInterface.php @@ -28,5 +28,5 @@ * You can use this method to help with memory management by breaking circular references. * Note: the Workflow logger may not emit logs from within the destroy method. */ -#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_CLASS)] class WorkflowInterface {} diff --git a/src/Workflow/WorkflowMethod.php b/src/Workflow/WorkflowMethod.php index 4195f84b4..d73c69ff0 100644 --- a/src/Workflow/WorkflowMethod.php +++ b/src/Workflow/WorkflowMethod.php @@ -20,7 +20,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_METHOD)] final class WorkflowMethod { /** diff --git a/src/Workflow/WorkflowVersioningBehavior.php b/src/Workflow/WorkflowVersioningBehavior.php index 605e9c020..c664456f1 100644 --- a/src/Workflow/WorkflowVersioningBehavior.php +++ b/src/Workflow/WorkflowVersioningBehavior.php @@ -25,7 +25,7 @@ * * @see \Temporal\Api\Enums\V1\VersioningBehavior */ -#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] +#[\Attribute(\Attribute::TARGET_METHOD)] final class WorkflowVersioningBehavior { public function __construct( diff --git a/testing/src/WorkerMock.php b/testing/src/WorkerMock.php index a725dbfa9..5ade2e99d 100644 --- a/testing/src/WorkerMock.php +++ b/testing/src/WorkerMock.php @@ -43,7 +43,7 @@ public function dispatch(ServerRequestInterface $request, array $headers): Promi return $this->wrapped->dispatch($request, $headers); } - public function getID(): string + public function getID(): int|string { return $this->wrapped->getID(); } From f04dd4fe183bb5ee8d2c9f306933b059c21442e0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 14 Mar 2026 09:44:06 +0000 Subject: [PATCH 2/5] style(php-cs-fixer): fix coding standards --- src/Activity.php | 1 + src/Activity/ActivityContextInterface.php | 1 + src/DataConverter/EncodedValues.php | 1 + src/DataConverter/ValuesInterface.php | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Activity.php b/src/Activity.php index 166313bdc..9e79c2b4e 100644 --- a/src/Activity.php +++ b/src/Activity.php @@ -95,6 +95,7 @@ public static function hasHeartbeatDetails(): bool * * This method retrieves the payload that was passed into the last call of the {@see Activity::heartbeat()} method. * + * @param null|mixed $type * @psalm-param TType $type * @throws OutOfContextException in the absence of the activity execution context. */ diff --git a/src/Activity/ActivityContextInterface.php b/src/Activity/ActivityContextInterface.php index 47e413524..e912dfe70 100644 --- a/src/Activity/ActivityContextInterface.php +++ b/src/Activity/ActivityContextInterface.php @@ -49,6 +49,7 @@ public function hasHeartbeatDetails(): bool; * * @see Activity::getHeartbeatDetails() * + * @param null|mixed $type * @psalm-param TType $type */ public function getLastHeartbeatDetails($type = null): mixed; diff --git a/src/DataConverter/EncodedValues.php b/src/DataConverter/EncodedValues.php index 79d681bbf..c94ca3219 100644 --- a/src/DataConverter/EncodedValues.php +++ b/src/DataConverter/EncodedValues.php @@ -76,6 +76,7 @@ public static function sliceValues( /** * Decode promise response upon returning it to the domain layer. * + * @param null|mixed $type * @psalm-param TType $type */ public static function decodePromise(PromiseInterface $promise, $type = null): PromiseInterface diff --git a/src/DataConverter/ValuesInterface.php b/src/DataConverter/ValuesInterface.php index cfe5ca91d..6053d2e0e 100644 --- a/src/DataConverter/ValuesInterface.php +++ b/src/DataConverter/ValuesInterface.php @@ -28,7 +28,7 @@ public function isEmpty(): bool; /** * @return void */ - public function setDataConverter(DataConverterInterface $converter); + public function setDataConverter(DataConverterInterface $converter): void; /** * Get value by it's index. From 6b032af66ce1be2636b786a1758e56765e9ba18b Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sun, 15 Mar 2026 12:24:42 +0400 Subject: [PATCH 3/5] feat: enhance compatibility and static analysis for testing - Added `testing/src` to `psalm.xml` for better coverage. - Unified OS constants using `OperatingSystem` across mappings. - Improved handling of environment variables and type safety in `SystemInfo` and `Environment`. - Refactored `RoadRunnerActivityInvocationCache` and `WorkerMock` for cleaner interfaces. - Adjusted `Downloader` to use a working directory for asset management. --- psalm-baseline.xml | 27 ++++++++++--- psalm.xml | 1 + .../RoadRunnerActivityInvocationCache.php | 2 +- src/WorkerFactory.php | 2 +- testing/src/ActivityMocker.php | 8 +++- testing/src/Command.php | 21 ++++++---- testing/src/Downloader.php | 17 ++++++-- testing/src/Environment.php | 39 +++++++++++++------ testing/src/Replay/WorkflowReplayer.php | 11 ++++-- testing/src/SystemInfo.php | 36 +++++++++-------- testing/src/TestService.php | 2 +- testing/src/WorkerFactory.php | 5 ++- testing/src/WorkerMock.php | 37 ++++++++++-------- testing/src/WorkflowTestCase.php | 6 ++- tests/Acceptance/App/Runtime/RRStarter.php | 3 +- tests/Functional/bootstrap.php | 4 +- 16 files changed, 145 insertions(+), 76 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 96f157a69..481492f99 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1146,9 +1146,6 @@ - - - getOptions()['name']]]> @@ -1350,9 +1347,6 @@ - - - @@ -1371,4 +1365,25 @@ + + + + toArray()['assets']]]> + + + + + + + + + + + + + + + + + diff --git a/psalm.xml b/psalm.xml index 4bc82f158..3e1d498a1 100644 --- a/psalm.xml +++ b/psalm.xml @@ -17,6 +17,7 @@ > + diff --git a/src/Worker/ActivityInvocationCache/RoadRunnerActivityInvocationCache.php b/src/Worker/ActivityInvocationCache/RoadRunnerActivityInvocationCache.php index d70f90833..0ecfbcab5 100644 --- a/src/Worker/ActivityInvocationCache/RoadRunnerActivityInvocationCache.php +++ b/src/Worker/ActivityInvocationCache/RoadRunnerActivityInvocationCache.php @@ -41,7 +41,7 @@ public function clear(): void $this->cache->clear(); } - public function saveCompletion(string $activityMethodName, $value): void + public function saveCompletion(string $activityMethodName, mixed $value): void { $this->cache->set($activityMethodName, ActivityInvocationResult::fromValue($value, $this->dataConverter)); } diff --git a/src/WorkerFactory.php b/src/WorkerFactory.php index a8496f2e7..146387ced 100644 --- a/src/WorkerFactory.php +++ b/src/WorkerFactory.php @@ -111,6 +111,7 @@ public function __construct( ?ServiceCredentials $credentials = null, ) { $this->converter = $dataConverter; + $this->codec = $this->createCodec(); $this->boot($credentials ?? ServiceCredentials::create()); } @@ -190,7 +191,6 @@ public function getEnvironment(): EnvironmentInterface public function run(?HostConnectionInterface $host = null): int { $host ??= RoadRunner::create(); - $this->codec = $this->createCodec(); while ($msg = $host->waitBatch()) { try { diff --git a/testing/src/ActivityMocker.php b/testing/src/ActivityMocker.php index 8ef6e52b7..0c45d26d7 100644 --- a/testing/src/ActivityMocker.php +++ b/testing/src/ActivityMocker.php @@ -21,11 +21,17 @@ public function clear(): void $this->cache->clear(); } - public function expectCompletion(string $activityMethodName, $value): void + /** + * @param non-empty-string $activityMethodName + */ + public function expectCompletion(string $activityMethodName, mixed $value): void { $this->cache->saveCompletion($activityMethodName, $value); } + /** + * @param non-empty-string $activityMethodName + */ public function expectFailure(string $activityMethodName, \Throwable $error): void { $this->cache->saveFailure($activityMethodName, $error); diff --git a/testing/src/Command.php b/testing/src/Command.php index 8c25dbdc8..6c49cd86e 100644 --- a/testing/src/Command.php +++ b/testing/src/Command.php @@ -12,14 +12,15 @@ final class Command /** @var non-empty-string|null Temporal Address */ public ?string $address = null; - /** @var non-empty-string|null */ public ?string $tlsKey = null; - /** @var non-empty-string|null */ public ?string $tlsCert = null; - private array $xdebug; + private array $xdebug = []; + /** + * @param non-empty-string|null $address + */ public function __construct( ?string $address = null, ) { @@ -28,9 +29,11 @@ public function __construct( public static function fromEnv(): self { - $self = new self(\getenv('TEMPORAL_ADDRESS') ?: '127.0.0.1:7233'); + $address = \getenv('TEMPORAL_ADDRESS'); + $self = new self((is_string($address) && $address !== '') ? $address : '127.0.0.1:7233'); - $self->namespace = \getenv('TEMPORAL_NAMESPACE') ?: 'default'; + $namespace = \getenv('TEMPORAL_NAMESPACE'); + $self->namespace = (is_string($namespace) && $namespace !== '') ? $namespace : 'default'; $self->xdebug = [ 'xdebug.mode' => \ini_get('xdebug.mode'), 'xdebug.start_with_request' => \ini_get('xdebug.start_with_request'), @@ -70,13 +73,17 @@ public static function fromCommandLine(array $argv): self break; } } + if (empty($address)) { + throw new \InvalidArgumentException('Address cannot be empty'); + } + if (empty($namespace)) { + throw new \InvalidArgumentException('Namespace cannot be empty'); + } $self = new self($address); - $self->namespace = $namespace; $self->tlsCert = $tlsCert; $self->tlsKey = $tlsKey; - return $self; } diff --git a/testing/src/Downloader.php b/testing/src/Downloader.php index 08ac97b63..f95e4f9f2 100644 --- a/testing/src/Downloader.php +++ b/testing/src/Downloader.php @@ -15,6 +15,7 @@ final class Downloader private Filesystem $filesystem; private HttpClientInterface $httpClient; private string $javaSdkUrl; + private string $workingDir; public function __construct( Filesystem $filesystem, @@ -27,15 +28,23 @@ public function __construct( $javaSdkVersion === self::TAG_LATEST => self::TAG_LATEST, default => "tags/$javaSdkVersion", }; + + $workingDir = \getcwd(); + if ($workingDir === false) { + throw new \RuntimeException('Failed to get current working directory.'); + } + + $this->workingDir = $workingDir; } public function download(SystemInfo $systemInfo): void { $asset = $this->getAsset($systemInfo); + /** @var string $assetUrl */ $assetUrl = $asset['browser_download_url']; $pathToExtractedAsset = $this->downloadAsset($assetUrl); - $targetPath = \getcwd() . DIRECTORY_SEPARATOR . $systemInfo->temporalServerExecutable; + $targetPath = $this->workingDir . DIRECTORY_SEPARATOR . $systemInfo->temporalServerExecutable; $this->filesystem->copy($pathToExtractedAsset . DIRECTORY_SEPARATOR . $systemInfo->temporalServerExecutable, $targetPath); $this->filesystem->chmod($targetPath, 0755); $this->filesystem->remove($pathToExtractedAsset); @@ -78,7 +87,7 @@ private function findAsset(array $assets, SystemInfo $systemInfo): array private function downloadAsset(string $assetUrl): string { $response = $this->httpClient->request('GET', $assetUrl); - $assetPath = \getcwd() . DIRECTORY_SEPARATOR . \basename($assetUrl); + $assetPath = $this->workingDir . DIRECTORY_SEPARATOR . \basename($assetUrl); if ($this->filesystem->exists($assetPath)) { $this->filesystem->remove($assetPath); @@ -87,9 +96,9 @@ private function downloadAsset(string $assetUrl): string $this->filesystem->appendToFile($assetPath, $response->getContent()); $phar = new \PharData($assetPath); - $extractedPath = \getcwd() . DIRECTORY_SEPARATOR . $phar->getFilename(); + $extractedPath = $this->workingDir . DIRECTORY_SEPARATOR . $phar->getFilename(); if (!$this->filesystem->exists($extractedPath)) { - $phar->extractTo(\getcwd()); + $phar->extractTo($this->workingDir); } $this->filesystem->remove($phar->getPath()); diff --git a/testing/src/Environment.php b/testing/src/Environment.php index 2fa8119ee..e510bfcbe 100644 --- a/testing/src/Environment.php +++ b/testing/src/Environment.php @@ -36,16 +36,19 @@ public function __construct( public static function create(?Command $command = null): self { - $token = \getenv('GITHUB_TOKEN'); - $systemInfo = SystemInfo::detect(); - \is_string(\getenv('ROADRUNNER_BINARY')) and $systemInfo->rrExecutable = \getenv('ROADRUNNER_BINARY'); + $roadRunnerBinary = \getenv('ROADRUNNER_BINARY'); + if (\is_string($roadRunnerBinary)) { + $systemInfo->rrExecutable = $roadRunnerBinary; + } + + $token = \getenv('GITHUB_TOKEN'); return new self( new TestOutputStyle(new ArgvInput(), new ConsoleOutput()), new Downloader(new Filesystem(), HttpClient::create([ 'headers' => [ - 'authorization' => $token ? 'token ' . $token : null, + 'authorization' => \is_string($token) ? 'token ' . $token : null, ], ])), $systemInfo, @@ -122,7 +125,7 @@ public function startTemporalServer( $this->io->info('Running command: ' . $this->serializeProcess($this->temporalServerProcess)); $this->temporalServerProcess->start(); - $deadline = \microtime(true) + $commandTimeout; + $deadline = \microtime(true) + (float) $commandTimeout; while (!$temporalStarted && \microtime(true) < $deadline) { \usleep(10_000); $check = new Process([ @@ -162,7 +165,7 @@ public function startTemporalTestServer(int $commandTimeout = 10): void $this->io->info('Temporal test server downloaded.'); } - $temporalPort = \parse_url($this->command->address, PHP_URL_PORT); + $temporalPort = \parse_url((string) $this->command->address, PHP_URL_PORT); $this->io->info('Starting Temporal test server... '); $this->temporalTestServerProcess = new Process( @@ -192,7 +195,7 @@ public function startTemporalTestServer(int $commandTimeout = 10): void /** * @param array $envs */ - public function startRoadRunner(?string $rrCommand = null, int $commandTimeout = 10, array $envs = [], string $configFile = '.rr.yaml'): void + public function startRoadRunner(?array $rrCommand = null, int $commandTimeout = 10, array $envs = [], string $configFile = '.rr.yaml'): void { if (!$this->isTemporalRunning() && !$this->isTemporalTestRunning()) { $this->io->error([ @@ -202,7 +205,7 @@ public function startRoadRunner(?string $rrCommand = null, int $commandTimeout = } $this->roadRunnerProcess = new Process( - command: $rrCommand ? \explode(' ', $rrCommand) : [$this->systemInfo->rrExecutable, 'serve'], + command: $rrCommand ?? [$this->systemInfo->rrExecutable, 'serve'], env: $envs, ); $this->roadRunnerProcess->setTimeout($commandTimeout); @@ -213,7 +216,7 @@ public function startRoadRunner(?string $rrCommand = null, int $commandTimeout = $this->roadRunnerProcess->start(); // wait for roadrunner to start - $deadline = \microtime(true) + $commandTimeout; + $deadline = \microtime(true) + (float) $commandTimeout; while (!$roadRunnerStarted && \microtime(true) < $deadline) { \usleep(10_000); $check = new Process([$this->systemInfo->rrExecutable, 'workers', '-c', $configFile]); @@ -289,26 +292,38 @@ public function stopRoadRunner(): void } } + /** + * @psalm-assert Process $this->temporalServerProcess + */ public function isTemporalRunning(): bool { return $this->temporalServerProcess?->isRunning() === true; } + /** + * @psalm-assert Process $this->roadRunnerProcess + */ public function isRoadRunnerRunning(): bool { return $this->roadRunnerProcess?->isRunning() === true; } + /** + * @psalm-assert Process $this->temporalTestServerProcess + */ public function isTemporalTestRunning(): bool { return $this->temporalTestServerProcess?->isRunning() === true; } - private function serializeProcess(?Process $temporalServerProcess): string|array + private function serializeProcess(?Process $process): string { - $reflection = new \ReflectionClass($temporalServerProcess); + if ($process === null) { + return 'process is not started'; + } + $reflection = new \ReflectionClass($process); $reflectionProperty = $reflection->getProperty('commandline'); - $commandLine = $reflectionProperty->getValue($temporalServerProcess); + $commandLine = $reflectionProperty->getValue($process); return \implode(' ', $commandLine); } } diff --git a/testing/src/Replay/WorkflowReplayer.php b/testing/src/Replay/WorkflowReplayer.php index 325f1f1ab..bbcdcbef4 100644 --- a/testing/src/Replay/WorkflowReplayer.php +++ b/testing/src/Replay/WorkflowReplayer.php @@ -34,7 +34,8 @@ final class WorkflowReplayer public function __construct() { - $this->rpc = new RPC(Relay::create(Environment::fromGlobals()->getRPCAddress()), new ProtobufCodec()); + $rpcAddress = Environment::fromGlobals()->getRPCAddress(); + $this->rpc = new RPC(Relay::create(!empty($rpcAddress) ? $rpcAddress : 'tcp://127.0.0.1:6001'), new ProtobufCodec()); } /** @@ -44,7 +45,6 @@ public function __construct() */ public function replayHistory(History $history): void { - /** @var HistoryEvent|null $firstEvent */ $firstEvent = $history->getEvents()[0] ?? null; $workflowType = $firstEvent?->getWorkflowExecutionStartedEventAttributes()?->getWorkflowType()?->getName() ?? throw new \LogicException('History is empty or broken.'); @@ -110,7 +110,10 @@ public function replayFromJSON( $this->sendRequest('temporal.ReplayFromJSON', $request); } - private function sendRequest(string $command, Message $request): ReplayResponse + /** + * @param non-empty-string $command + */ + private function sendRequest(string $command, Message $request): void { $wfType = (string) $request->getWorkflowType()?->getName(); try { @@ -132,7 +135,7 @@ private function sendRequest(string $command, Message $request): ReplayResponse \assert($status !== null); if ($status->getCode() === 0) { - return $message; + return; } throw match ($status->getCode()) { diff --git a/testing/src/SystemInfo.php b/testing/src/SystemInfo.php index 353c85a66..b63adcf34 100644 --- a/testing/src/SystemInfo.php +++ b/testing/src/SystemInfo.php @@ -10,9 +10,9 @@ final class SystemInfo { private const PLATFORM_MAPPINGS = [ - 'darwin' => 'macOS', - 'linux' => 'linux', - 'windows' => 'windows', + OperatingSystem::OS_DARWIN => 'macOS', + OperatingSystem::OS_LINUX => 'linux', + OperatingSystem::OS_WINDOWS => 'windows', ]; private const ARCHITECTURE_MAPPINGS = [ 'x64' => 'amd64', @@ -20,19 +20,19 @@ final class SystemInfo 'arm64' => 'arm64', ]; private const TEMPORAL_EXECUTABLE_MAP = [ - 'darwin' => './temporal-test-server', - 'linux' => './temporal-test-server', - 'windows' => 'temporal-test-server.exe', + OperatingSystem::OS_DARWIN => './temporal-test-server', + OperatingSystem::OS_LINUX => './temporal-test-server', + OperatingSystem::OS_WINDOWS => 'temporal-test-server.exe', ]; private const TEMPORAL_CLI_EXECUTABLE_MAP = [ - 'darwin' => './temporal', - 'linux' => './temporal', - 'windows' => 'temporal.exe', + OperatingSystem::OS_DARWIN => './temporal', + OperatingSystem::OS_LINUX => './temporal', + OperatingSystem::OS_WINDOWS => 'temporal.exe', ]; private const RR_EXECUTABLE_MAP = [ - 'darwin' => './rr', - 'linux' => './rr', - 'windows' => 'rr.exe', + OperatingSystem::OS_DARWIN => './rr', + OperatingSystem::OS_LINUX => './rr', + OperatingSystem::OS_WINDOWS => 'rr.exe', ]; public string $arch; @@ -61,14 +61,16 @@ private function __construct( public static function detect(): self { $os = OperatingSystem::createFromGlobals(); + $architecture = Architecture::createFromGlobals(); + $rrBinary = \getenv('ROADRUNNER_BINARY'); return new self( $os, - self::PLATFORM_MAPPINGS[$os], - self::ARCHITECTURE_MAPPINGS[Architecture::createFromGlobals()], - self::TEMPORAL_EXECUTABLE_MAP[$os], - \getenv('ROADRUNNER_BINARY') ?: self::RR_EXECUTABLE_MAP[$os], - self::TEMPORAL_CLI_EXECUTABLE_MAP[$os], + self::PLATFORM_MAPPINGS[$os] ?? self::PLATFORM_MAPPINGS[OperatingSystem::OS_LINUX], + self::ARCHITECTURE_MAPPINGS[$architecture] ?? self::ARCHITECTURE_MAPPINGS['amd64'], + self::TEMPORAL_EXECUTABLE_MAP[$os] ?? self::TEMPORAL_EXECUTABLE_MAP[OperatingSystem::OS_LINUX], + (is_string($rrBinary) && $rrBinary !== '') ? $rrBinary : (self::RR_EXECUTABLE_MAP[$os] ?? self::RR_EXECUTABLE_MAP[OperatingSystem::OS_LINUX]), + self::TEMPORAL_CLI_EXECUTABLE_MAP[$os] ?? self::TEMPORAL_CLI_EXECUTABLE_MAP[OperatingSystem::OS_LINUX], ); } } diff --git a/testing/src/TestService.php b/testing/src/TestService.php index 26260bbec..bdb283a47 100644 --- a/testing/src/TestService.php +++ b/testing/src/TestService.php @@ -108,7 +108,7 @@ public function getCurrentTime(): Carbon { /** @var GetCurrentTimeResponse $result */ $result = $this->invoke('GetCurrentTime', new GPBEmpty()); - return Carbon::createFromTimestamp($result->getTime()->getSeconds()); + return Carbon::createFromTimestamp($result->getTime()?->getSeconds() ?? 0); } private function invoke(string $method, object $request): object diff --git a/testing/src/WorkerFactory.php b/testing/src/WorkerFactory.php index 181b3bca0..d5bc82626 100644 --- a/testing/src/WorkerFactory.php +++ b/testing/src/WorkerFactory.php @@ -38,6 +38,9 @@ public function __construct( parent::__construct($dataConverter, $rpc, $credentials ?? ServiceCredentials::create()); } + /** + * @psalm-suppress UnsafeInstantiation + */ public static function create( ?DataConverterInterface $converter = null, ?RPCConnectionInterface $rpc = null, @@ -63,7 +66,7 @@ public function newWorker( $worker = new WorkerMock( new Worker( $taskQueue, - $options ?? WorkerOptions::new(), + $options, ServiceContainer::fromWorkerFactory( $this, $exceptionInterceptor ?? ExceptionInterceptor::createDefault(), diff --git a/testing/src/WorkerMock.php b/testing/src/WorkerMock.php index 5ade2e99d..6e9ec2885 100644 --- a/testing/src/WorkerMock.php +++ b/testing/src/WorkerMock.php @@ -7,27 +7,24 @@ use React\Promise\PromiseInterface; use Temporal\Internal\Events\EventEmitterTrait; use Temporal\Internal\Events\EventListenerInterface; -use Temporal\Internal\Repository\RepositoryInterface; use Temporal\Worker\ActivityInvocationCache\ActivityInvocationCacheInterface; use Temporal\Worker\DispatcherInterface; use Temporal\Worker\Transport\Command\ServerRequestInterface; use Temporal\Worker\WorkerInterface; use Temporal\Worker\WorkerOptions; +/** + * @template-implements EventListenerInterface + */ final class WorkerMock implements WorkerInterface, EventListenerInterface, DispatcherInterface { + /** @use EventEmitterTrait */ use EventEmitterTrait; - private WorkerInterface $wrapped; - private ActivityInvocationCacheInterface $activityInvocationCache; - public function __construct( - WorkerInterface $wrapped, - ActivityInvocationCacheInterface $activityInvocationCache, - ) { - $this->wrapped = $wrapped; - $this->activityInvocationCache = $activityInvocationCache; - } + private WorkerInterface&DispatcherInterface $wrapped, + private ActivityInvocationCacheInterface $activityInvocationCache, + ) {} public function getOptions(): WorkerOptions { @@ -50,30 +47,38 @@ public function getID(): int|string public function registerWorkflowTypes(string ...$class): WorkerInterface { - return $this->wrapped->registerWorkflowTypes(...$class); + $this->wrapped->registerWorkflowTypes(...$class); + + return $this; } - public function getWorkflows(): RepositoryInterface + public function getWorkflows(): iterable { return $this->wrapped->getWorkflows(); } public function registerActivityImplementations(object ...$activity): WorkerInterface { - return $this->wrapped->registerActivityImplementations(...$activity); + $this->wrapped->registerActivityImplementations(...$activity); + + return $this; } public function registerActivity(string $type, ?callable $factory = null): WorkerInterface { - return $this->wrapped->registerActivity($type, $factory); + $this->wrapped->registerActivity($type, $factory); + + return $this; } public function registerActivityFinalizer(\Closure $finalizer): WorkerInterface { - return $this->wrapped->registerActivityFinalizer($finalizer); + $this->wrapped->registerActivityFinalizer($finalizer); + + return $this; } - public function getActivities(): RepositoryInterface + public function getActivities(): iterable { return $this->wrapped->getActivities(); } diff --git a/testing/src/WorkflowTestCase.php b/testing/src/WorkflowTestCase.php index 6a64f9e72..57dbb2fa6 100644 --- a/testing/src/WorkflowTestCase.php +++ b/testing/src/WorkflowTestCase.php @@ -8,6 +8,9 @@ use Temporal\Client\GRPC\ServiceClient; use Temporal\Client\WorkflowClient; +/** + * @psalm-suppress PropertyNotSetInConstructor + */ class WorkflowTestCase extends TestCase { protected WorkflowClient $workflowClient; @@ -15,7 +18,8 @@ class WorkflowTestCase extends TestCase protected function setUp(): void { - $temporalAddress = \getenv('TEMPORAL_ADDRESS') ?: '127.0.0.1:7233'; + $temporalAddress = \getenv('TEMPORAL_ADDRESS'); + $temporalAddress = is_string($temporalAddress) && !empty($temporalAddress) ? $temporalAddress : '127.0.0.1:7233'; $this->workflowClient = new WorkflowClient(ServiceClient::create($temporalAddress)); $this->testingService = TestService::create($temporalAddress); diff --git a/tests/Acceptance/App/Runtime/RRStarter.php b/tests/Acceptance/App/Runtime/RRStarter.php index 0e826be57..76b3930db 100644 --- a/tests/Acceptance/App/Runtime/RRStarter.php +++ b/tests/Acceptance/App/Runtime/RRStarter.php @@ -46,11 +46,10 @@ public function start(): void ]; $run->tlsKey === null or $rrCommand = [...$rrCommand, '-o', "tls.key={$run->tlsKey}"]; $run->tlsCert === null or $rrCommand = [...$rrCommand, '-o', "tls.cert={$run->tlsCert}"]; - $command = \implode(' ', $rrCommand); // echo "\e[1;36mStart RoadRunner with command:\e[0m {$command}\n"; $this->environment->startRoadRunner( - rrCommand: $command, + rrCommand: $rrCommand, configFile: $this->runtime->rrConfigDir . DIRECTORY_SEPARATOR . '.rr.yaml', ); } diff --git a/tests/Functional/bootstrap.php b/tests/Functional/bootstrap.php index c8837af0b..e52282867 100644 --- a/tests/Functional/bootstrap.php +++ b/tests/Functional/bootstrap.php @@ -15,7 +15,7 @@ $environment->startTemporalTestServer(); (new SearchAttributeTestInvoker())(); $environment->startRoadRunner( - rrCommand: \implode(' ', [ + rrCommand: [ $systemInfo->rrExecutable, 'serve', '-c', '.rr.silent.yaml', @@ -27,7 +27,7 @@ 'worker.php', ...$environment->command->getCommandLineArguments(), ]), - ]), + ], configFile: 'tests/Functional/.rr.silent.yaml', ); From 4ae80530be7ccdbc03c673f38031217149c8dcd0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 15 Mar 2026 08:42:19 +0000 Subject: [PATCH 4/5] style(php-cs-fixer): fix coding standards --- src/DataConverter/ValuesInterface.php | 3 --- testing/src/Command.php | 6 ++---- testing/src/Replay/WorkflowReplayer.php | 1 - testing/src/SystemInfo.php | 2 +- testing/src/WorkflowTestCase.php | 2 +- 5 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/DataConverter/ValuesInterface.php b/src/DataConverter/ValuesInterface.php index 6053d2e0e..7854c7486 100644 --- a/src/DataConverter/ValuesInterface.php +++ b/src/DataConverter/ValuesInterface.php @@ -25,9 +25,6 @@ interface ValuesInterface extends \Countable */ public function isEmpty(): bool; - /** - * @return void - */ public function setDataConverter(DataConverterInterface $converter): void; /** diff --git a/testing/src/Command.php b/testing/src/Command.php index 6c49cd86e..ad24b86bb 100644 --- a/testing/src/Command.php +++ b/testing/src/Command.php @@ -13,9 +13,7 @@ final class Command public ?string $address = null; public ?string $tlsKey = null; - public ?string $tlsCert = null; - private array $xdebug = []; /** @@ -30,10 +28,10 @@ public function __construct( public static function fromEnv(): self { $address = \getenv('TEMPORAL_ADDRESS'); - $self = new self((is_string($address) && $address !== '') ? $address : '127.0.0.1:7233'); + $self = new self((\is_string($address) && $address !== '') ? $address : '127.0.0.1:7233'); $namespace = \getenv('TEMPORAL_NAMESPACE'); - $self->namespace = (is_string($namespace) && $namespace !== '') ? $namespace : 'default'; + $self->namespace = (\is_string($namespace) && $namespace !== '') ? $namespace : 'default'; $self->xdebug = [ 'xdebug.mode' => \ini_get('xdebug.mode'), 'xdebug.start_with_request' => \ini_get('xdebug.start_with_request'), diff --git a/testing/src/Replay/WorkflowReplayer.php b/testing/src/Replay/WorkflowReplayer.php index bbcdcbef4..a3e0f8a1f 100644 --- a/testing/src/Replay/WorkflowReplayer.php +++ b/testing/src/Replay/WorkflowReplayer.php @@ -14,7 +14,6 @@ use Temporal\Api\Common\V1\WorkflowExecution; use Temporal\Api\Common\V1\WorkflowType; use Temporal\Api\History\V1\History; -use Temporal\Api\History\V1\HistoryEvent; use Temporal\Client\GRPC\StatusCode; use Temporal\Testing\Replay\Exception\InternalServerException; use Temporal\Testing\Replay\Exception\InvalidArgumentException; diff --git a/testing/src/SystemInfo.php b/testing/src/SystemInfo.php index b63adcf34..74fd293d6 100644 --- a/testing/src/SystemInfo.php +++ b/testing/src/SystemInfo.php @@ -69,7 +69,7 @@ public static function detect(): self self::PLATFORM_MAPPINGS[$os] ?? self::PLATFORM_MAPPINGS[OperatingSystem::OS_LINUX], self::ARCHITECTURE_MAPPINGS[$architecture] ?? self::ARCHITECTURE_MAPPINGS['amd64'], self::TEMPORAL_EXECUTABLE_MAP[$os] ?? self::TEMPORAL_EXECUTABLE_MAP[OperatingSystem::OS_LINUX], - (is_string($rrBinary) && $rrBinary !== '') ? $rrBinary : (self::RR_EXECUTABLE_MAP[$os] ?? self::RR_EXECUTABLE_MAP[OperatingSystem::OS_LINUX]), + (\is_string($rrBinary) && $rrBinary !== '') ? $rrBinary : (self::RR_EXECUTABLE_MAP[$os] ?? self::RR_EXECUTABLE_MAP[OperatingSystem::OS_LINUX]), self::TEMPORAL_CLI_EXECUTABLE_MAP[$os] ?? self::TEMPORAL_CLI_EXECUTABLE_MAP[OperatingSystem::OS_LINUX], ); } diff --git a/testing/src/WorkflowTestCase.php b/testing/src/WorkflowTestCase.php index 57dbb2fa6..95b394269 100644 --- a/testing/src/WorkflowTestCase.php +++ b/testing/src/WorkflowTestCase.php @@ -19,7 +19,7 @@ class WorkflowTestCase extends TestCase protected function setUp(): void { $temporalAddress = \getenv('TEMPORAL_ADDRESS'); - $temporalAddress = is_string($temporalAddress) && !empty($temporalAddress) ? $temporalAddress : '127.0.0.1:7233'; + $temporalAddress = \is_string($temporalAddress) && !empty($temporalAddress) ? $temporalAddress : '127.0.0.1:7233'; $this->workflowClient = new WorkflowClient(ServiceClient::create($temporalAddress)); $this->testingService = TestService::create($temporalAddress); From 8e6fabe6ed07e37a1592dbfa3b49237135fdae53 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sun, 15 Mar 2026 13:24:13 +0400 Subject: [PATCH 5/5] refactor: adjust attribute declarations for NamedArgumentConstructor compatibility - Updated attribute declarations across Workflow and Activity classes for better compatibility with `NamedArgumentConstructor`. - Removed unnecessary property and constructor in `LocalActivityInterface`. - Updated `psalm-baseline.xml` to reflect changes and deprecations. --- psalm-baseline.xml | 55 +++++++++++++++++++++ src/Activity/ActivityInterface.php | 2 +- src/Activity/ActivityMethod.php | 2 +- src/Activity/LocalActivityInterface.php | 25 +--------- src/Workflow/QueryMethod.php | 2 +- src/Workflow/ReturnType.php | 2 +- src/Workflow/SignalMethod.php | 2 +- src/Workflow/UpdateMethod.php | 2 +- src/Workflow/UpdateValidatorMethod.php | 2 +- src/Workflow/WorkflowInterface.php | 2 +- src/Workflow/WorkflowMethod.php | 2 +- src/Workflow/WorkflowVersioningBehavior.php | 2 +- 12 files changed, 67 insertions(+), 33 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 481492f99..253b4b014 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,20 @@ + + + + + + + + + + + + + + + @@ -1365,6 +1380,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Activity/ActivityInterface.php b/src/Activity/ActivityInterface.php index 446bfcaa3..fe2fbb6c9 100644 --- a/src/Activity/ActivityInterface.php +++ b/src/Activity/ActivityInterface.php @@ -32,7 +32,7 @@ * @NamedArgumentConstructor * @Target({ "CLASS" }) */ -#[\Attribute(\Attribute::TARGET_CLASS)] +#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor] class ActivityInterface { /** diff --git a/src/Activity/ActivityMethod.php b/src/Activity/ActivityMethod.php index 71137725b..4942402b1 100644 --- a/src/Activity/ActivityMethod.php +++ b/src/Activity/ActivityMethod.php @@ -20,7 +20,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class ActivityMethod { /** diff --git a/src/Activity/LocalActivityInterface.php b/src/Activity/LocalActivityInterface.php index da6d71861..f53fdbe9c 100644 --- a/src/Activity/LocalActivityInterface.php +++ b/src/Activity/LocalActivityInterface.php @@ -12,7 +12,6 @@ namespace Temporal\Activity; use Doctrine\Common\Annotations\Annotation\Target; -use JetBrains\PhpStorm\Immutable; use Spiral\Attributes\NamedArgumentConstructor; /** @@ -32,25 +31,5 @@ * @NamedArgumentConstructor * @Target({ "CLASS" }) */ -#[\Attribute(\Attribute::TARGET_CLASS)] -final class LocalActivityInterface extends ActivityInterface -{ - /** - * Prefix to prepend to method names to generate activity types. Default is - * empty string which means that method names are used as activity types. - * - * Note that this value is ignored if a name of an activity is specified - * explicitly through {@see ActivityMethod::$name}. - * - * Be careful about names that contain special characters. These names can - * be used as metric tags. And systems like prometheus ignore metrics which - * have tags with unsupported characters. - */ - #[Immutable] - public string $prefix = ''; - - public function __construct(string $prefix = '') - { - $this->prefix = $prefix; - } -} +#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor] +final class LocalActivityInterface extends ActivityInterface {} diff --git a/src/Workflow/QueryMethod.php b/src/Workflow/QueryMethod.php index 001efd49c..e23257aaf 100644 --- a/src/Workflow/QueryMethod.php +++ b/src/Workflow/QueryMethod.php @@ -26,7 +26,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class QueryMethod { /** diff --git a/src/Workflow/ReturnType.php b/src/Workflow/ReturnType.php index bac8b1142..dff3030dd 100644 --- a/src/Workflow/ReturnType.php +++ b/src/Workflow/ReturnType.php @@ -20,7 +20,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class ReturnType { public const TYPE_ANY = Type::TYPE_ANY; diff --git a/src/Workflow/SignalMethod.php b/src/Workflow/SignalMethod.php index 9a6bb0cdf..58fc7c020 100644 --- a/src/Workflow/SignalMethod.php +++ b/src/Workflow/SignalMethod.php @@ -23,7 +23,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class SignalMethod { /** diff --git a/src/Workflow/UpdateMethod.php b/src/Workflow/UpdateMethod.php index d32b16c87..a5657eb17 100644 --- a/src/Workflow/UpdateMethod.php +++ b/src/Workflow/UpdateMethod.php @@ -22,7 +22,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class UpdateMethod { /** diff --git a/src/Workflow/UpdateValidatorMethod.php b/src/Workflow/UpdateValidatorMethod.php index 9324753b3..cd2c85f3d 100644 --- a/src/Workflow/UpdateValidatorMethod.php +++ b/src/Workflow/UpdateValidatorMethod.php @@ -23,7 +23,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class UpdateValidatorMethod { /** diff --git a/src/Workflow/WorkflowInterface.php b/src/Workflow/WorkflowInterface.php index 63e7cf9f3..5f1703018 100644 --- a/src/Workflow/WorkflowInterface.php +++ b/src/Workflow/WorkflowInterface.php @@ -28,5 +28,5 @@ * You can use this method to help with memory management by breaking circular references. * Note: the Workflow logger may not emit logs from within the destroy method. */ -#[\Attribute(\Attribute::TARGET_CLASS)] +#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor] class WorkflowInterface {} diff --git a/src/Workflow/WorkflowMethod.php b/src/Workflow/WorkflowMethod.php index d73c69ff0..4195f84b4 100644 --- a/src/Workflow/WorkflowMethod.php +++ b/src/Workflow/WorkflowMethod.php @@ -20,7 +20,7 @@ * @NamedArgumentConstructor * @Target({ "METHOD" }) */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class WorkflowMethod { /** diff --git a/src/Workflow/WorkflowVersioningBehavior.php b/src/Workflow/WorkflowVersioningBehavior.php index c664456f1..605e9c020 100644 --- a/src/Workflow/WorkflowVersioningBehavior.php +++ b/src/Workflow/WorkflowVersioningBehavior.php @@ -25,7 +25,7 @@ * * @see \Temporal\Api\Enums\V1\VersioningBehavior */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class WorkflowVersioningBehavior { public function __construct(