diff --git a/src/EntityContext.php b/src/EntityContext.php index 153239d8..b814c307 100644 --- a/src/EntityContext.php +++ b/src/EntityContext.php @@ -41,9 +41,11 @@ use Bottledcode\DurablePhp\Events\WithOrchestration; use Bottledcode\DurablePhp\Exceptions\Unwind; use Bottledcode\DurablePhp\Glue\Provenance; +use Bottledcode\DurablePhp\Proxy\SpyException; use Bottledcode\DurablePhp\Proxy\SpyProxy; use Bottledcode\DurablePhp\State\EntityHistory; use Bottledcode\DurablePhp\State\EntityId; +use Bottledcode\DurablePhp\State\EntityState; use Bottledcode\DurablePhp\State\Ids\StateId; use Closure; use Crell\Serde\Attributes\ClassSettings; @@ -111,29 +113,6 @@ public function setState(mixed $value): void $this->state = $value; } - public function signalEntity( - EntityId $entityId, - string $operation, - array $input = [], - ?DateTimeImmutable $scheduledTime = null, - ): void { - $event = $this->addFrom( - WithEntity::forInstance( - StateId::fromEntityId($entityId), - RaiseEvent::forOperation($operation, $input), - ), - ); - if ($scheduledTime) { - $event = WithDelay::forEvent($scheduledTime, $event); - } - $this->eventDispatcher->fire($event); - } - - private function addFrom(Event $event): Event - { - return WithFrom::forEvent($this->from, $event); - } - public function getId(): EntityId { return $this->id; @@ -161,6 +140,11 @@ public function startNewOrchestration(string $orchestration, array $input = [], ); } + private function addFrom(Event $event): Event + { + return WithFrom::forEvent($this->from, $event); + } + public function delay(Closure $self, DateTimeInterface $until = new DateTimeImmutable()): void { $classReflector = new ReflectionClass($this->history->getState()); @@ -283,4 +267,47 @@ public function revokeRole(string $role): void ), ); } + + public function signal(EntityId $entityId, callable $signal): void + { + $proxy = $this->spyProxy->define($entityId->name); + $operationName = null; + $arguments = null; + $proxy = new $proxy($operationName, $arguments); + try { + $proxy($signal); + + if ($operationName === null || $arguments === null) { + return; + } + } catch (SpyException) { + $this->signalEntity($entityId, $operationName, $arguments ?? []); + } + } + + public function signalEntity( + EntityId $entityId, + string $operation, + array $input = [], + ?DateTimeImmutable $scheduledTime = null, + ): void { + $event = $this->addFrom( + WithEntity::forInstance( + StateId::fromEntityId($entityId), + RaiseEvent::forOperation($operation, $input), + ), + ); + if ($scheduledTime) { + $event = WithDelay::forEvent($scheduledTime, $event); + } + $this->eventDispatcher->fire($event); + } + + public function getSnapshot(EntityId $entityId): EntityState + { + $state = $this->eventDispatcher->getState(StateId::fromEntityId($entityId)); + assert($state instanceof EntityHistory); + + return $state->getState(); + } } diff --git a/src/EntityContextInterface.php b/src/EntityContextInterface.php index adc2683a..117a6991 100644 --- a/src/EntityContextInterface.php +++ b/src/EntityContextInterface.php @@ -26,6 +26,7 @@ use Bottledcode\DurablePhp\Events\Shares\Operation; use Bottledcode\DurablePhp\State\EntityId; +use Bottledcode\DurablePhp\State\EntityState; use Closure; use Crell\Serde\Attributes\ClassNameTypeMap; use DateTimeImmutable; @@ -102,6 +103,26 @@ public function getId(): EntityId; */ public function getOperation(): string; + /** + * Call the entity with a single signal + * + * @template T of EntityState + * + * @param EntityId $entityId + * @param callable(T): void $signal + */ + public function signal(EntityId $entityId, callable $signal): void; + + /** + * Retrieve a snapshot of the remote entity state + * + * @template T of EntityState + * + * @param EntityId $entityId + * @return EntityState + */ + public function getSnapshot(EntityId $entityId): EntityState; + public function startNewOrchestration(string $orchestration, array $input = [], ?string $id = null): void; public function delayUntil( diff --git a/src/Proxy/Generator.php b/src/Proxy/Generator.php index c575540f..1e232607 100644 --- a/src/Proxy/Generator.php +++ b/src/Proxy/Generator.php @@ -34,8 +34,13 @@ abstract class Generator { - public function __construct(protected string|null $cacheDir = null) {} + public function __construct(protected ?string $cacheDir = null) {} + /** + * Ensures that the given interface is implemented + * + * @throws \ReflectionException + */ public function define(string $interface): string { $name = $this->getName($class = new ReflectionClass($interface)); @@ -45,6 +50,7 @@ public function define(string $interface): string $cacheFile = $this->cacheDir . DIRECTORY_SEPARATOR . $name . '.php'; if (file_exists($cacheFile)) { require_once $cacheFile; + return '\\' . $namespace . '\\' . $name; } } @@ -52,7 +58,7 @@ public function define(string $interface): string $reflection = new ReflectionClass($interface); $fullname = $this->getInterfaceNamespace($reflection) . '\\' . $this->getName($reflection); - if (!class_exists($fullname)) { + if (! class_exists($fullname)) { eval($output = $this->generate($interface)); if ($cacheFile) { file_put_contents($cacheFile, "pureMethod($hook, true); } $hooks[] = '}'; + return implode( "\n", array_filter( @@ -115,6 +122,7 @@ function (ReflectionMethod $method) { ); }, $props); $props = implode("\n", $props); + return <<isBuiltin()) { return $nullable . $type->getName(); } + return $nullable . '\\' . $type->getName(); } diff --git a/src/State/EntityId.php b/src/State/EntityId.php index 22886302..cbc31101 100644 --- a/src/State/EntityId.php +++ b/src/State/EntityId.php @@ -28,10 +28,13 @@ use Withinboredom\Record; /** - * @template T + * @template T of EntityState */ readonly class EntityId extends Record implements Stringable { + /** + * @var class-string + */ public protected(set) string $name; public protected(set) string $id;