Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion src/Internal/Declaration/Prototype/WorkflowCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,37 @@
namespace Temporal\Internal\Declaration\Prototype;

use Temporal\Internal\Repository\ArrayRepository;
use Temporal\Internal\Repository\Identifiable;

/**
* @template-extends ArrayRepository<WorkflowPrototype>
*/
final class WorkflowCollection extends ArrayRepository {}
final class WorkflowCollection extends ArrayRepository
{
/**
* A dynamic (catch-all) workflow is the single fallback used when no
* statically registered workflow matches the requested type. As in the
* other SDKs (Go panics, Python raises), at most one may be registered per
* worker — a second would make dispatch ambiguous.
*/
public function add(Identifiable $entry, bool $overwrite = false): void
{
if ($entry instanceof WorkflowPrototype && $entry->isDynamic()) {
foreach ($this as $existing) {
if ($existing instanceof WorkflowPrototype
&& $existing->isDynamic()
&& $existing->getID() !== $entry->getID()
) {
throw new \LogicException(\sprintf(
'Cannot register dynamic workflow "%s": a dynamic (catch-all) workflow "%s" is '
. 'already registered. At most one dynamic workflow is allowed per worker.',
$entry->getID(),
$existing->getID(),
));
}
}
}

parent::add($entry, $overwrite);
}
}
15 changes: 15 additions & 0 deletions src/Internal/Declaration/Prototype/WorkflowPrototype.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ final class WorkflowPrototype extends Prototype
private ?MethodRetry $methodRetry = null;
private ?ReturnType $returnType = null;
private bool $hasInitializer = false;
private bool $dynamic = false;
private VersioningBehavior $versioningBehavior;

/**
Expand Down Expand Up @@ -72,6 +73,20 @@ public function setHasInitializer(bool $hasInitializer): void
$this->hasInitializer = $hasInitializer;
}

/**
* Whether this is the dynamic (catch-all) workflow, invoked when no
* statically registered workflow matches the requested type name.
*/
public function isDynamic(): bool
{
return $this->dynamic;
}

public function setDynamic(bool $dynamic): void
{
$this->dynamic = $dynamic;
}

public function getCronSchedule(): ?CronSchedule
{
return $this->cronSchedule;
Expand Down
5 changes: 4 additions & 1 deletion src/Internal/Declaration/Reader/WorkflowReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,9 @@ private function findProto(

$name = $info->name ?? $interface->getShortName();

return new WorkflowPrototype($name, $handler, $class);
$prototype = new WorkflowPrototype($name, $handler, $class);
$prototype->setDynamic($info->dynamic);

return $prototype;
}
}
3 changes: 3 additions & 0 deletions src/Internal/Transport/Router/GetWorkerInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ private function workerToArray(WorkerInterface $worker): array
'queries' => \array_keys($workflow->getQueryHandlers()),
'signals' => \array_keys($workflow->getSignalHandlers()),
'versioning_behavior' => $workflow->getVersioningBehavior()->value,
// Advertise the dynamic (catch-all) workflow so the RoadRunner
// temporal plugin registers a Go dynamic-workflow proxy for it.
'dynamic' => $workflow->isDynamic(),
];

$activityMap = static fn(ActivityPrototype $activity): array => [
Expand Down
16 changes: 15 additions & 1 deletion src/Internal/Transport/Router/StartWorkflow.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,21 @@ public function handle(ServerRequestInterface $request, array $headers, Deferred

private function findWorkflowOrFail(WorkflowInfo $info): WorkflowPrototype
{
return $this->services->workflows->find($info->type->name) ?? throw new \OutOfRangeException(
$found = $this->services->workflows->find($info->type->name);

if ($found instanceof WorkflowPrototype) {
return $found;
}

// Fall back to the dynamic (catch-all) workflow, if one is registered.
// The handler reads the real type name via Workflow::getInfo()->type.
foreach ($this->services->workflows as $prototype) {
if ($prototype instanceof WorkflowPrototype && $prototype->isDynamic()) {
return $prototype;
}
}

throw new \OutOfRangeException(
\sprintf(self::ERROR_NOT_FOUND, $info->type->name),
);
}
Expand Down
14 changes: 13 additions & 1 deletion src/Workflow/WorkflowMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,23 @@ final class WorkflowMethod
#[Immutable]
public ?string $name = null;

/**
* Marks this as a dynamic (catch-all) workflow: it is invoked when the
* worker receives a workflow whose type name is not statically registered.
* At most one dynamic workflow may be registered per worker. The handler
* reads the actual type name via {@see \Temporal\Workflow::getInfo()} and
* receives the raw arguments (declare a {@see \Temporal\DataConverter\ValuesInterface}
* parameter to access them).
*/
#[Immutable]
public bool $dynamic = false;

/**
* @param non-empty-string|null $name
*/
public function __construct(?string $name = null)
public function __construct(?string $name = null, bool $dynamic = false)
{
$this->name = $name;
$this->dynamic = $dynamic;
}
}
26 changes: 26 additions & 0 deletions tests/Unit/Declaration/Fixture/WorkflowWithAnotherDynamic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

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

declare(strict_types=1);

namespace Temporal\Tests\Unit\Declaration\Fixture;

use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;

/** @WorkflowInterface */
#[WorkflowInterface]
class WorkflowWithAnotherDynamic
{
/** @WorkflowMethod(dynamic=true) */
#[WorkflowMethod(dynamic: true)]
public function handler(): void
{
}
}
26 changes: 26 additions & 0 deletions tests/Unit/Declaration/Fixture/WorkflowWithDynamic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

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

declare(strict_types=1);

namespace Temporal\Tests\Unit\Declaration\Fixture;

use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;

/** @WorkflowInterface */
#[WorkflowInterface]
class WorkflowWithDynamic
{
/** @WorkflowMethod(dynamic=true) */
#[WorkflowMethod(dynamic: true)]
public function handler(): void
{
}
}
30 changes: 30 additions & 0 deletions tests/Unit/Declaration/WorkflowDeclarationTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
use Spiral\Attributes\AttributeReader;
use Temporal\Common\CronSchedule;
use Temporal\Internal\Declaration\Instantiator\WorkflowInstantiator;
use Temporal\Internal\Declaration\Prototype\WorkflowCollection;
use Temporal\Internal\Declaration\Prototype\WorkflowPrototype;
use Temporal\Internal\Declaration\Reader\WorkflowReader;
use Temporal\Tests\Unit\Declaration\Fixture\Inheritance\ExtendingWorkflow;
use Temporal\Tests\Unit\Declaration\Fixture\SimpleWorkflow;
use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithAnotherDynamic;
use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithConstructor;
use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithCron;
use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithCronAndRetry;
use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithCustomName;
use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithDynamic;
use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithoutHandler;
use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithQueries;
use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithRetry;
Expand All @@ -51,6 +54,33 @@ public function testWorkflowWithoutHandler(WorkflowReader $reader): void
$this->assertNull($prototype->getHandler());
}

/**
* @param WorkflowReader $reader
* @throws \ReflectionException
*/
#[TestDox("Reading a dynamic (catch-all) workflow sets the dynamic flag")]
#[DataProvider('workflowReaderDataProvider')]
public function testDynamicWorkflow(WorkflowReader $reader): void
{
$this->assertTrue($reader->fromClass(WorkflowWithDynamic::class)->isDynamic());
$this->assertFalse($reader->fromClass(SimpleWorkflow::class)->isDynamic());
}

/**
* @param WorkflowReader $reader
* @throws \ReflectionException
*/
#[TestDox("At most one dynamic workflow may be registered per worker")]
#[DataProvider('workflowReaderDataProvider')]
public function testMultipleDynamicWorkflowsAreRejected(WorkflowReader $reader): void
{
$collection = new WorkflowCollection();
$collection->add($reader->fromClass(WorkflowWithDynamic::class));

$this->expectException(\LogicException::class);
$collection->add($reader->fromClass(WorkflowWithAnotherDynamic::class));
}

/**
* @param WorkflowReader $reader
* @throws \ReflectionException
Expand Down