diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 10e3ad5fd..253b4b014 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -1,10 +1,5 @@
-
-
-
-
-
@@ -15,21 +10,11 @@
-
-
-
-
-
-
-
-
-
-
@@ -40,17 +25,6 @@
-
-
- current()]]>
-
-
-
-
-
-
-
-
@@ -274,62 +248,16 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- values]]>
-
-
-
-
-
-
-
- getName()]]>
-
-
- getCode()]]>
- getCode()]]>
-
-
- name]]>
-
-
-
-
- getName()]]>
-
-
-
-
- getName()]]>
-
-
-
-
-
-
@@ -638,25 +566,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
last]]>
@@ -1079,9 +988,6 @@
-
- getReturnType()]]>
-
@@ -1105,9 +1011,6 @@
start(...$args)->then(fn() => $this->getResult($returnType))]]>
-
-
-
@@ -1258,9 +1161,6 @@
-
-
-
getOptions()['name']]]>
@@ -1462,9 +1362,6 @@
-
-
-
@@ -1483,16 +1380,6 @@
-
-
-
-
-
-
-
-
-
-
@@ -1503,11 +1390,6 @@
-
-
- getCode()]]>
-
-
@@ -1538,4 +1420,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/Activity.php b/src/Activity.php
index 8bc210c8a..9e79c2b4e 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,8 @@ 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
+ * @param null|mixed $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..e912dfe70 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,8 @@ public function hasHeartbeatDetails(): bool;
*
* @see Activity::getHeartbeatDetails()
*
- * @param Type|string $type
+ * @param null|mixed $type
+ * @psalm-param TType $type
*/
public function getLastHeartbeatDetails($type = null): mixed;
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..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;
/**
@@ -33,24 +32,4 @@
* @Target({ "CLASS" })
*/
#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor]
-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;
- }
-}
+final class LocalActivityInterface extends ActivityInterface {}
diff --git a/src/Activity/LocalActivityOptions.php b/src/Activity/LocalActivityOptions.php
index d8d896ea8..dd722db39 100644
--- a/src/Activity/LocalActivityOptions.php
+++ b/src/Activity/LocalActivityOptions.php
@@ -94,7 +94,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..c94ca3219 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,8 @@ public static function sliceValues(
/**
* Decode promise response upon returning it to the domain layer.
*
- * @param string|\ReflectionClass|\ReflectionType|Type|null $type
+ * @param null|mixed $type
+ * @psalm-param TType $type
*/
public static function decodePromise(PromiseInterface $promise, $type = null): PromiseInterface
{
@@ -91,7 +94,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 +108,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 +211,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..7854c7486 100644
--- a/src/DataConverter/ValuesInterface.php
+++ b/src/DataConverter/ValuesInterface.php
@@ -25,7 +25,7 @@ interface ValuesInterface extends \Countable
*/
public function isEmpty(): bool;
- public function setDataConverter(DataConverterInterface $converter);
+ public function setDataConverter(DataConverterInterface $converter): void;
/**
* Get value by it's index.
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/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/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/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/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..ad24b86bb 100644
--- a/testing/src/Command.php
+++ b/testing/src/Command.php
@@ -12,14 +12,13 @@ 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 +27,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 +71,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..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;
@@ -34,7 +33,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 +44,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 +109,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 +134,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..74fd293d6 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 a725dbfa9..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
{
@@ -43,37 +40,45 @@ 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();
}
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..95b394269 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',
);