diff --git a/src/config.php b/src/config.php index e7194bf..eb566a6 100644 --- a/src/config.php +++ b/src/config.php @@ -88,8 +88,7 @@ ->constructorParameter('drain_timeout', get('dapr.actors.drain_timeout')) ->constructorParameter('drain_enabled', get('dapr.actors.drain_enabled')), ActorRuntime::class => autowire() - ->constructorParameter('logger', get('dapr.logger')) - ->constructorParameter('deserializer', get('dapr.internal.deserializer')), + ->constructorParameter('client', get(\Dapr\Client\DaprClient::class)), ActorState::class => autowire()->constructorParameter('logger', get('dapr.logger')), ActorProxy::class => autowire()->constructorParameter('logger', get('dapr.logger')), ApplicationJson::class => autowire(), @@ -143,6 +142,7 @@ TransactionalState::class => autowire()->constructorParameter('logger', get('dapr.logger')), // default application settings + \Dapr\Actors\Internal\Caches\CacheInterface::class => autowire(\Dapr\Actors\Internal\Caches\MemoryCache::class), 'dapr.pubsub.default' => 'pubsub', 'dapr.actors.proxy.generation' => ProxyFactory::GENERATED, 'dapr.subscriptions' => [], diff --git a/src/index.php b/src/index.php index 57c59ec..9de7e07 100644 --- a/src/index.php +++ b/src/index.php @@ -86,11 +86,11 @@ function (ProxyFactory $proxyFactory, DaprClient $client, LoggerInterface $logge data: ['amount' => 2], period: new DateInterval('PT10M') ); - $actor->create_reminder($reminder, $client); + $actor->create_reminder($reminder); $logger->critical('Created reminder'); sleep(2); $body = assert_equals($body, 3, $actor->get_count(), 'Reminder should increment'); - $read_reminder = $actor->get_reminder('increment', $client); + $read_reminder = $actor->get_reminder('increment'); $logger->critical('Got reminder'); $body = assert_equals( $body, @@ -106,15 +106,15 @@ function (ProxyFactory $proxyFactory, DaprClient $client, LoggerInterface $logge callback: 'increment', data: 2 ); - $actor->create_timer($timer, $client); + $actor->create_timer($timer); $logger->critical('Created timer'); sleep(2); $body = assert_equals($body, 5, $actor->get_count(), 'Timer should increment'); - $actor->delete_timer('increment', $client); - $actor->delete_reminder('increment', $client); - $actor->delete_reminder('nope', $client); - $actor->delete_timer('nope', $client); + $actor->delete_timer('increment'); + $actor->delete_reminder('increment'); + $actor->delete_reminder('nope'); + $actor->delete_timer('nope'); $logger->critical('Cleaned up'); $object = new SimpleObject(); diff --git a/src/lib/Actors/ActorRuntime.php b/src/lib/Actors/ActorRuntime.php index 94f5971..98b1443 100644 --- a/src/lib/Actors/ActorRuntime.php +++ b/src/lib/Actors/ActorRuntime.php @@ -2,18 +2,17 @@ namespace Dapr\Actors; +use Dapr\Actors\Internal\Caches\CacheInterface; use Dapr\Actors\Internal\Caches\FileCache; -use Dapr\Actors\Internal\Caches\NoCache; -use Dapr\Deserialization\IDeserializer; +use Dapr\Client\DaprClient; use Dapr\exceptions\DaprException; use Dapr\exceptions\Http\NotFound; use Dapr\exceptions\SaveStateFailure; -use DI\Container; use DI\DependencyException; use DI\FactoryInterface; use DI\NotFoundException; use Exception; -use Psr\Log\LoggerInterface; +use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; use ReflectionNamedType; @@ -28,11 +27,10 @@ class ActorRuntime { public function __construct( - protected LoggerInterface $logger, protected ActorConfig $actor_config, protected FactoryInterface $factory, - protected Container $container, - protected IDeserializer $deserializer, + protected ContainerInterface $container, + protected DaprClient $client ) { } @@ -49,7 +47,7 @@ public function __construct( public function do_method(IActor $actor, string $method, mixed $arg): mixed { $reflection = new ReflectionClass($actor); - $method = $reflection->getMethod($method); + $method = $reflection->getMethod($method); if (empty($arg)) { return $method->invoke($actor); } @@ -61,18 +59,18 @@ public function do_method(IActor $actor, string $method, mixed $arg): mixed return $method->invokeArgs( $actor, - [$parameter->getName() => $this->deserializer->detect_from_parameter($parameter, $arg)] + [$parameter->getName() => $this->client->deserializer->detect_from_parameter($parameter, $arg)] ); } public function deactivate_actor(IActor $actor, string $dapr_type): void { - $id = $actor->get_id(); - $activation_tracker = hash('sha256', $dapr_type.$id); + $id = $actor->get_id(); + $activation_tracker = hash('sha256', $dapr_type . $id); $activation_tracker = rtrim( - sys_get_temp_dir(), - DIRECTORY_SEPARATOR - ).DIRECTORY_SEPARATOR.'dapr_'.$activation_tracker; + sys_get_temp_dir(), + DIRECTORY_SEPARATOR + ) . DIRECTORY_SEPARATOR . 'dapr_' . $activation_tracker; $is_activated = file_exists($activation_tracker); @@ -86,21 +84,20 @@ public function deactivate_actor(IActor $actor, string $dapr_type): void /** * Resolve an actor, then calls your callback with the actor before committing any state changes * - * @param string $dapr_type The dapr type to resolve - * @param string $id The id of the actor to resolve + * @param ActorReference $reference * @param callable $loan A callback that takes an IActor * * @return mixed The result of you callback * @throws NotFound * @throws SaveStateFailure */ - public function resolve_actor(string $dapr_type, string $id, callable $loan): mixed + public function resolve_actor(ActorReference $reference, callable $loan): mixed { try { - $reflection = $this->locate_actor($dapr_type); + $reflection = $this->locate_actor($reference); $this->validate_actor($reflection); - $states = $this->get_states($reflection, $dapr_type, $id); - $actor = $this->get_actor($reflection, $dapr_type, $id, $states); + $states = $this->get_states($reflection, $reference, $this->client); + $actor = $this->get_actor($reflection, $reference, $states); // @codeCoverageIgnoreStart } catch (Exception $exception) { throw new NotFound('Actor could not be located', previous: $exception); @@ -123,18 +120,17 @@ public function resolve_actor(string $dapr_type, string $id, callable $loan): mi /** * Locates an actor implementation * - * @param string $dapr_type The dapr type to locate - * + * @param ActorReference $reference The actor reference * @return ReflectionClass * @throws NotFound * @throws ReflectionException */ - protected function locate_actor(string $dapr_type): ReflectionClass + protected function locate_actor(ActorReference $reference): ReflectionClass { - $type = $this->actor_config->get_actor_type_from_dapr_type($dapr_type); - if ( ! class_exists($type)) { + $type = $this->actor_config->get_actor_type_from_dapr_type($reference->get_actor_type()); + if (!class_exists($type)) { // @codeCoverageIgnoreStart - $this->logger->critical('Unable to locate an actor for {t}', ['t' => $type]); + $this->client->logger->critical('Unable to locate an actor for {t}', ['t' => $type]); throw new NotFound(); // @codeCoverageIgnoreEnd } @@ -156,7 +152,7 @@ protected function validate_actor(ReflectionClass $reflection): bool } // @codeCoverageIgnoreStart - $this->logger->critical('Actor does not implement the IActor interface'); + $this->client->logger->critical('Actor does not implement the IActor interface'); throw new NotFound(); // @codeCoverageIgnoreEnd } @@ -165,18 +161,19 @@ protected function validate_actor(ReflectionClass $reflection): bool * Retrieves an array of states for a located actor * * @param ReflectionClass $reflection The class we're loading states for - * @param string $dapr_type The dapr type - * @param string $id The id - * + * @param ActorReference $reference * @return array - * @throws ReflectionException * @throws DependencyException * @throws NotFoundException + * @throws ReflectionException */ - protected function get_states(ReflectionClass $reflection, string $dapr_type, string $id): array - { + protected function get_states( + ReflectionClass $reflection, + ActorReference $reference, + DaprClient $client + ): array { $constructor = $reflection->getMethod('__construct'); - $states = []; + $states = []; foreach ($constructor->getParameters() as $parameter) { $type = $parameter->getType(); if ($type instanceof ReflectionNamedType) { @@ -185,10 +182,16 @@ protected function get_states(ReflectionClass $reflection, string $dapr_type, st $reflected_type = new ReflectionClass($type_name); if ($reflected_type->isSubclassOf(ActorState::class)) { $state = $this->container->make($type_name); - $this->begin_transaction($state, $reflected_type, $dapr_type, $id); + $this->begin_transaction( + $state, + $reflected_type, + $reference, + $client, + $this->get_cache_for_actor($reference, $type_name) + ); $states[$parameter->name] = $state; - $this->logger?->debug('Found state {t}', ['t' => $type_name]); + $this->client->logger?->debug('Found state {t}', ['t' => $type_name]); } } } @@ -211,16 +214,18 @@ protected function get_states(ReflectionClass $reflection, string $dapr_type, st protected function begin_transaction( ActorState $state, ReflectionClass $reflected_type, - string $dapr_type, - string $actor_id, + ActorReference $reference, + DaprClient $client, + CacheInterface $cache, ?ReflectionClass $original = null ): void { if ($reflected_type->name !== ActorState::class) { $this->begin_transaction( $state, $reflected_type->getParentClass(), - $dapr_type, - $actor_id, + $reference, + $client, + $cache, $original ?? $reflected_type ); @@ -228,7 +233,12 @@ protected function begin_transaction( } $begin_transaction = $reflected_type->getMethod('begin_transaction'); $begin_transaction->setAccessible(true); - $begin_transaction->invoke($state, $dapr_type, $actor_id); + $begin_transaction->invoke($state, $reference, $client, $cache); + } + + protected function get_cache_for_actor(ActorReference $reference, string $type_name): CacheInterface + { + return $this->factory->make(CacheInterface::class, ['reference' => $reference, 'state_name' => $type_name]); } /** @@ -243,23 +253,23 @@ protected function begin_transaction( * @throws DependencyException * @throws NotFoundException */ - protected function get_actor(ReflectionClass $reflection, string $dapr_type, string $id, array $states): IActor + protected function get_actor(ReflectionClass $reflection, ActorReference $reference, array $states): IActor { - $states['id'] = $id; - $this->container->set(ActorReference::class, new ActorReference($id, $dapr_type)); - $actor = $this->factory->make($reflection->getName(), $states); - $activation_tracker = hash('sha256', $dapr_type.$id); + $states['id'] = $reference->get_actor_id(); + $this->container->set(ActorReference::class, $reference); + $actor = $this->factory->make($reflection->getName(), $states); + $activation_tracker = hash('sha256', $reference->get_actor_type() . $reference->get_actor_id()); $activation_tracker = rtrim( - sys_get_temp_dir(), - DIRECTORY_SEPARATOR - ).DIRECTORY_SEPARATOR.'dapr_'.$activation_tracker; + sys_get_temp_dir(), + DIRECTORY_SEPARATOR + ) . DIRECTORY_SEPARATOR . 'dapr_' . $activation_tracker; $is_activated = file_exists($activation_tracker); - if ( ! $is_activated) { - $this->logger?->info( + if (!$is_activated) { + $this->client->logger?->info( 'Activating {type}||{id}', - ['type' => $dapr_type, 'id' => $id] + ['type' => $reference->get_actor_type(), 'id' => $reference->get_actor_id()] ); touch($activation_tracker); $actor->on_activation(); diff --git a/src/lib/Actors/ActorState.php b/src/lib/Actors/ActorState.php index fa26832..51dd93d 100644 --- a/src/lib/Actors/ActorState.php +++ b/src/lib/Actors/ActorState.php @@ -4,19 +4,12 @@ use Dapr\Actors\Internal\Caches\CacheInterface; use Dapr\Actors\Internal\Caches\KeyNotFound; -use Dapr\Actors\Internal\Caches\MemoryCache; -use Dapr\Actors\Internal\Caches\NoCache; -use Dapr\Actors\Internal\KeyResponse; -use Dapr\DaprClient; -use Dapr\Deserialization\IDeserializer; +use Dapr\Client\DaprClient; use Dapr\exceptions\DaprException; use Dapr\State\Internal\Transaction; use DI\DependencyException; -use DI\FactoryInterface; use DI\NotFoundException; use InvalidArgumentException; -use Psr\Container\ContainerInterface; -use Psr\Log\LoggerInterface; use ReflectionClass; use ReflectionException; use ReflectionProperty; @@ -34,15 +27,12 @@ abstract class ActorState * @var string[] Where values came from, to determine whether to load the value from the store. */ private Transaction $transaction; - private LoggerInterface $logger; - private IDeserializer $deserializer; private ReflectionClass $reflection; private DaprClient $client; - private string $actor_id; - private string $dapr_type; + private ActorReference $reference; private CacheInterface $cache; - public function __construct(private ContainerInterface $container, private FactoryInterface $factory) + public function __construct() { } @@ -55,19 +45,15 @@ public function __construct(private ContainerInterface $container, private Facto */ public function save_state(): void { - $this->logger->debug( + $this->client->logger->debug( 'Committing transaction for {t}||{i}', ['t' => $this->dapr_type, 'i' => $this->actor_id] ); - $operations = $this->transaction->get_transaction(); - if (empty($operations)) { - return; - } - $this->client->post("/actors/{$this->dapr_type}/{$this->actor_id}/state", $operations); + $this->client->saveActorState($this->reference, $this->transaction); $this->cache->flush_cache(); - $this->begin_transaction($this->dapr_type, $this->actor_id); + $this->begin_transaction($this->reference, $this->client, $this->cache); } /** @@ -79,31 +65,22 @@ public function save_state(): void * @throws DependencyException * @throws NotFoundException */ - private function begin_transaction(string $dapr_type, string $actor_id) + private function begin_transaction(ActorReference $reference, DaprClient $client, CacheInterface $cache) { - $this->dapr_type = $dapr_type; - $this->actor_id = $actor_id; - $this->reflection = new ReflectionClass($this); - $this->logger = $this->container->get(LoggerInterface::class); - $this->deserializer = $this->container->get(IDeserializer::class); - $this->client = $this->container->get(DaprClient::class); + $this->reference = $reference; + $this->reflection = new ReflectionClass($this); + $this->client = $client; foreach ($this->reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { unset($this->{$property->name}); } - $this->transaction = $this->factory->make(Transaction::class); - $this->logger->debug( + $this->transaction = new Transaction($client->serializer, $client->deserializer); + $client->logger->debug( 'Starting a new transaction for {t}||{i}', ['t' => $this->dapr_type, 'i' => $this->actor_id] ); - try { - $cache_type = $this->container->get('dapr.actors.cache'); - } catch (DependencyException | NotFoundException) { - $this->logger->warning('No cache type found, turning off actor state cache. Set `dapr.actors.cache`'); - $cache_type = MemoryCache::class; - } - $this->cache = new $cache_type($dapr_type, $actor_id, get_class($this)); + $this->cache = $cache; } /** @@ -113,11 +90,8 @@ public function roll_back(): void { // have to reset the cache, since we don't know the state because the transaction was rolled back $this->cache->reset(); - $this->logger->debug('Rolled back transaction'); - try { - $this->transaction = $this->factory->make(Transaction::class); - } catch (DependencyException | NotFoundException) { - } + $this->client->logger->debug('Rolled back transaction'); + $this->transaction = new Transaction($this->client->serializer, $this->client->deserializer); } /** @@ -150,9 +124,9 @@ public function __get(string $key): mixed */ public function __set(string $key, mixed $value): void { - if ( ! $this->reflection->hasProperty($key)) { + if (!$this->reflection->hasProperty($key)) { throw new InvalidArgumentException( - "$key on ".get_class($this)." is not defined and thus will not be stored." + "$key on " . get_class($this) . " is not defined and thus will not be stored." ); } $this->cache->set_key($key, $value); @@ -169,18 +143,18 @@ public function __set(string $key, mixed $value): void */ private function _load_key(string $key): mixed { - $state = $this->client->get("/actors/{$this->dapr_type}/{$this->actor_id}/state/$key"); $property = $this->reflection->getProperty($key); + $as = $this->client->deserializer->detect_type_name_from_property($property); - $value = match ($state->code) { - KeyResponse::SUCCESS => $this->deserializer->detect_from_property($property, $state->data), - KeyResponse::KEY_NOT_FOUND => $property->hasDefaultValue() ? $property->getDefaultValue() : null, - KeyResponse::ACTOR_NOT_FOUND => throw new DaprException('Actor not found!') - }; + try { + $state = $this->client->getActorState($this->reference, $key, $as); + } catch (KeyNotFound) { + $state = $property->hasDefaultValue() ? $property->getDefaultValue() : null; + } - $this->cache->set_key($key, $value); + $this->cache->set_key($key, $state); - return $value; + return $state; } /** diff --git a/src/lib/Actors/ActorTrait.php b/src/lib/Actors/ActorTrait.php index afcf4bc..98b6060 100644 --- a/src/lib/Actors/ActorTrait.php +++ b/src/lib/Actors/ActorTrait.php @@ -16,6 +16,9 @@ */ trait ActorTrait { + private \Dapr\Client\DaprClient $client; + private ActorReference $reference; + /** * Creates a reminder. These are persisted. * @@ -24,15 +27,18 @@ trait ActorTrait * @return bool True if successful * @throws DaprException */ - public function create_reminder(Reminder $reminder, DaprClient $client): bool + public function create_reminder(Reminder $reminder, ?DaprClient $client = null): bool { + if ($client === null) { + return $this->client->createActorReminder($this->_get_actor_reference(), $reminder); + } // inline function: get name if (isset($this->DAPR_TYPE)) { $type = $this->DAPR_TYPE; } else { - $class = new ReflectionClass($this); + $class = new ReflectionClass($this); $attributes = $class->getAttributes(DaprType::class); - if ( ! empty($attributes)) { + if (!empty($attributes)) { $type = $attributes[0]->newInstance()->type; } else { $type = $class->getShortName(); @@ -54,15 +60,18 @@ public function create_reminder(Reminder $reminder, DaprClient $client): bool * @return Reminder|null The reminder * @throws DaprException */ - public function get_reminder(string $name, DaprClient $client): ?Reminder + public function get_reminder(string $name, ?DaprClient $client = null): ?Reminder { + if ($client === null) { + return $this->client->getActorReminder($this->_get_actor_reference(), $name); + } // inline function: get name if (isset($this->DAPR_TYPE)) { $type = $this->DAPR_TYPE; } else { - $class = new ReflectionClass($this); + $class = new ReflectionClass($this); $attributes = $class->getAttributes(DaprType::class); - if ( ! empty($attributes)) { + if (!empty($attributes)) { $type = $attributes[0]->newInstance()->type; } else { $type = $class->getShortName(); @@ -84,15 +93,18 @@ public function get_reminder(string $name, DaprClient $client): ?Reminder * @return bool True if successful * @throws DaprException */ - public function delete_reminder(string $name, DaprClient $client): bool + public function delete_reminder(string $name, ?DaprClient $client = null): bool { + if ($client === null) { + return $this->client->deleteActorReminder($this->_get_actor_reference(), $name); + } // inline function: get name if (isset($this->DAPR_TYPE)) { $type = $this->DAPR_TYPE; } else { - $class = new ReflectionClass($this); + $class = new ReflectionClass($this); $attributes = $class->getAttributes(DaprType::class); - if ( ! empty($attributes)) { + if (!empty($attributes)) { $type = $attributes[0]->newInstance()->type; } else { $type = $class->getShortName(); @@ -114,15 +126,18 @@ public function delete_reminder(string $name, DaprClient $client): bool * @return bool True if successful * @throws DaprException */ - public function create_timer(Timer $timer, DaprClient $client): bool + public function create_timer(Timer $timer, ?DaprClient $client = null): bool { + if ($client === null) { + return $this->client->createActorTimer($this->_get_actor_reference(), $timer); + } // inline function: get name if (isset($this->DAPR_TYPE)) { $type = $this->DAPR_TYPE; } else { - $class = new ReflectionClass($this); + $class = new ReflectionClass($this); $attributes = $class->getAttributes(DaprType::class); - if ( ! empty($attributes)) { + if (!empty($attributes)) { $type = $attributes[0]->newInstance()->type; } else { $type = $class->getShortName(); @@ -144,15 +159,18 @@ public function create_timer(Timer $timer, DaprClient $client): bool * @return bool True if successful * @throws DaprException */ - public function delete_timer(string $name, DaprClient $client): bool + public function delete_timer(string $name, ?DaprClient $client = null): bool { + if ($client === null) { + return $this->client->deleteActorTimer($this->_get_actor_reference(), $name); + } // inline function: get name if (isset($this->DAPR_TYPE)) { $type = $this->DAPR_TYPE; } else { - $class = new ReflectionClass($this); + $class = new ReflectionClass($this); $attributes = $class->getAttributes(DaprType::class); - if ( ! empty($attributes)) { + if (!empty($attributes)) { $type = $attributes[0]->newInstance()->type; } else { $type = $class->getShortName(); diff --git a/src/lib/Actors/Attributes/Delete.php b/src/lib/Actors/Attributes/Delete.php new file mode 100644 index 0000000..1da5746 --- /dev/null +++ b/src/lib/Actors/Attributes/Delete.php @@ -0,0 +1,12 @@ +cache_dir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'dapr-proxy-cache'.DIRECTORY_SEPARATOR; } @@ -40,10 +40,7 @@ public function set_cache_dir(string $dir) { public function get_proxy(string $id): object { if ( ! class_exists($this->get_full_class_name())) { - $file_generator = $this->factory->make( - FileGenerator::class, - ['interface' => $this->interface, 'dapr_type' => $this->dapr_type] - ); + $file_generator = new FileGenerator($this->interface, $this->dapr_type, $this->client); $file = $file_generator->generate_file(); if ( ! is_dir($this->cache_dir)) { mkdir($this->cache_dir); diff --git a/src/lib/Actors/Generators/DynamicGenerator.php b/src/lib/Actors/Generators/DynamicGenerator.php index 75573f0..326888c 100644 --- a/src/lib/Actors/Generators/DynamicGenerator.php +++ b/src/lib/Actors/Generators/DynamicGenerator.php @@ -2,8 +2,13 @@ namespace Dapr\Actors\Generators; +use Dapr\Actors\ActorReference; +use Dapr\Actors\Attributes\Delete; +use Dapr\Actors\Attributes\Get; +use Dapr\Actors\Attributes\Post; +use Dapr\Actors\Attributes\Put; use Dapr\Actors\Internal\InternalProxy; -use Dapr\DaprClient; +use Dapr\Client\DaprClient; use DI\FactoryInterface; use JetBrains\PhpStorm\Pure; use LogicException; @@ -24,22 +29,33 @@ class DynamicGenerator extends GenerateProxy #[Pure] public function __construct( string $interface, string $dapr_type, - FactoryInterface $factory, - ContainerInterface $container + DaprClient $client ) { - parent::__construct($interface, $dapr_type, $factory, $container); + parent::__construct($interface, $dapr_type, $client); } public function get_proxy(string $id): InternalProxy { - $current_proxy = new InternalProxy(); - $interface = ClassType::from($this->interface); - $methods = $this->get_methods($interface); + $current_proxy = new InternalProxy(); + $interface = ClassType::from($this->interface); + $methods = $this->get_methods($interface); $current_proxy->DAPR_TYPE = $this->dapr_type; + + $reflection = new \ReflectionClass($current_proxy); + $client =$reflection->getProperty('client'); + $client->setAccessible(true); + $client->setValue($current_proxy, $this->client); + $reference = $reflection->getProperty('reference'); + $actor_reference = new ActorReference($id, $this->dapr_type); + $reference->setAccessible(true); + $reference->setValue($current_proxy, $actor_reference); + foreach ($methods as $method) { $current_proxy->{$method->getName()} = $this->generate_method($method, $id); } + $current_proxy->_get_actor_reference = fn() => $actor_reference; + return $current_proxy; } @@ -52,25 +68,37 @@ protected function generate_failure_method(Method $method): callable protected function generate_proxy_method(Method $method, string $id): callable { - return function (...$params) use ($method, $id) { - $serializer = $this->container->get('dapr.internal.serializer'); - $client = $this->container->get(DaprClient::class); - $deserializer = $this->container->get('dapr.internal.deserializer'); - if ( ! empty($params)) { - $params = $serializer->as_array($params[0]); - } + $http_method = count($method->getParameters()) == 0 ? 'GET' : 'POST'; + foreach ($method->getAttributes() as $attribute) { + $http_method = match ($attribute->getName()) { + Get::class => 'GET', + Post::class => 'POST', + Put::class => 'PUT', + Delete::class => 'DELETE', + default => $http_method + }; + } + $reference = new ActorReference($id, $this->dapr_type); + $actor_method = $method->getName(); + $return_type = $method->getReturnType(); - $result = $client->post( - "/actors/{$this->dapr_type}/$id/method/{$method->getName()}", - $serializer->as_array($params) + return function (...$params) use ($id, $http_method, $reference, $actor_method, $return_type) { + /** + * @var DaprClient $client + */ + $result = $this->client->invokeActorMethod( + $http_method, + $reference, + $actor_method, + $params[0] ?? null, + $return_type ?? 'array' ); - $result->data = $deserializer->detect_from_generator_method( - $method, - $result->data - ); + if ($return_type) { + return $result; + } - return $result->data; + return; }; } diff --git a/src/lib/Actors/Generators/ExistingOnly.php b/src/lib/Actors/Generators/ExistingOnly.php index 81a0f21..62fd0ec 100644 --- a/src/lib/Actors/Generators/ExistingOnly.php +++ b/src/lib/Actors/Generators/ExistingOnly.php @@ -3,13 +3,12 @@ namespace Dapr\Actors\Generators; use Dapr\Actors\IActor; +use Dapr\Client\DaprClient; use DI\DependencyException; -use DI\FactoryInterface; use DI\NotFoundException; use JetBrains\PhpStorm\Pure; use LogicException; use Nette\PhpGenerator\Method; -use Psr\Container\ContainerInterface; /** * Class ExistingOnly @@ -23,10 +22,9 @@ class ExistingOnly extends GenerateProxy #[Pure] public function __construct( string $interface, string $dapr_type, - FactoryInterface $factory, - ContainerInterface $container + DaprClient $client ) { - parent::__construct($interface, $dapr_type, $factory, $container); + parent::__construct($interface, $dapr_type, $client); } /** @@ -38,7 +36,8 @@ class ExistingOnly extends GenerateProxy */ public function get_proxy(string $id) { - $proxy = $this->factory->make($this->get_full_class_name()); + $reflection = new \ReflectionClass($this->get_full_class_name()); + $proxy = $reflection->newInstance($this->client); $proxy->id = $id; return $proxy; diff --git a/src/lib/Actors/Generators/FileGenerator.php b/src/lib/Actors/Generators/FileGenerator.php index f30d683..1f848b0 100644 --- a/src/lib/Actors/Generators/FileGenerator.php +++ b/src/lib/Actors/Generators/FileGenerator.php @@ -2,12 +2,15 @@ namespace Dapr\Actors\Generators; +use Dapr\Actors\ActorReference; use Dapr\Actors\ActorTrait; use Dapr\Actors\Attributes\DaprType; +use Dapr\Actors\Attributes\Delete; +use Dapr\Actors\Attributes\Get; +use Dapr\Actors\Attributes\Post; +use Dapr\Actors\Attributes\Put; use Dapr\Actors\IActor; -use Dapr\DaprClient; -use Dapr\Deserialization\IDeserializer; -use Dapr\Serialization\ISerializer; +use Dapr\Client\DaprClient; use DI\DependencyException; use DI\FactoryInterface; use DI\NotFoundException; @@ -17,7 +20,6 @@ use Nette\PhpGenerator\Method; use Nette\PhpGenerator\PhpFile; use Nette\PhpGenerator\Type; -use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; @@ -31,13 +33,12 @@ class FileGenerator extends GenerateProxy { #[Pure] public function __construct( - protected string $interface, - protected string $dapr_type, - FactoryInterface $factory, - ContainerInterface $container, + string $interface, + string $dapr_type, + DaprClient $client, private array $usings = [] ) { - parent::__construct($interface, $dapr_type, $factory, $container); + parent::__construct($interface, $dapr_type, $client); } /** @@ -57,7 +58,7 @@ public static function generate( string|null $override_type = null ): PhpFile { $reflected_interface = new ReflectionClass($interface); - $type = $override_type ?? ($reflected_interface->getAttributes( + $type = $override_type ?? ($reflected_interface->getAttributes( DaprType::class )[0] ?? null)?->newInstance()->type; @@ -77,12 +78,17 @@ public static function generate( */ public function get_proxy(string $id): IActor { - if ( ! class_exists($this->get_full_class_name())) { + if (!class_exists($this->get_full_class_name())) { foreach ($this->generate_file()->getNamespaces() as $namespace) { eval($namespace); } } - $proxy = $this->factory->make($this->get_full_class_name()); + $reflection = new ReflectionClass($this->get_full_class_name()); + + /** + * @var IActor $proxy + */ + $proxy = $reflection->newInstance($this->client); $proxy->id = $id; return $proxy; @@ -97,11 +103,13 @@ public function generate_file(): PhpFile $interface->setClass(); $interface->setName($this->get_short_class_name()); $interface->addTrait(ActorTrait::class); - $interface->addProperty('DAPR_TYPE', $this->dapr_type); + $interface->addProperty('DAPR_TYPE', $this->dapr_type)->setType(Type::STRING)->setPublic(); + $interface->addProperty('reference')->setPrivate()->setType(ActorReference::class); + $interface->setFinal(true); // maybe implement IActor $reflected_interface = new ReflectionClass($interface); - if ( ! $reflected_interface->isSubclassOf(IActor::class)) { + if (!$reflected_interface->isSubclassOf(IActor::class)) { $interface->addImplement(IActor::class); } @@ -116,6 +124,7 @@ public function generate_file(): PhpFile } } $interface->addMember($this->generate_constructor()); + $interface->addMember($this->generate_reference()); // configure file $file = new PhpFile(); @@ -124,9 +133,10 @@ public function generate_file(): PhpFile // configure namespace $namespace->add($interface); - $namespace->addUse('\Dapr\Actors\IActor'); - $namespace->addUse('\Dapr\Actors\Attributes\DaprType'); - $namespace->addUse('\Dapr\Actors\ActorTrait'); + $namespace->addUse(IActor::class); + $namespace->addUse(DaprType::class); + $namespace->addUse(ActorTrait::class); + $namespace->addUse(ActorReference::class); foreach ($this->usings as $using) { if (class_exists($using)) { $namespace->addUse($using); @@ -140,17 +150,22 @@ protected function generate_constructor(): Method { $method = new Method('__construct'); $method->addPromotedParameter('client')->setType(DaprClient::class)->setPrivate(); - $method->addPromotedParameter('serializer')->setType(ISerializer::class)->setPrivate(); - $method->addPromotedParameter('deserializer')->setType(IDeserializer::class)->setPrivate(); $method->setPublic(); $this->usings[] = DaprClient::class; - $this->usings[] = ISerializer::class; - $this->usings[] = IDeserializer::class; $method->setBody(''); return $method; } + protected function generate_reference(): Method + { + $method = new Method('_get_actor_reference'); + $method->setPrivate(); + $method->setReturnType(ActorReference::class); + $method->setBody('return $this->reference ??= new ActorReference($this->id, $this->DAPR_TYPE);'); + return $method; + } + /** * @inheritDoc */ @@ -158,7 +173,8 @@ protected function generate_proxy_method(Method $method, string $id): Method { $params = array_values($method->getParameters()); $method->setPublic(); - if ( ! empty($params)) { + $http_method = 'GET'; + if (!empty($params)) { // @codeCoverageIgnoreStart if (isset($params[1])) { throw new LogicException( @@ -173,26 +189,29 @@ protected function generate_proxy_method(Method $method, string $id): Method // @codeCoverageIgnoreEnd $this->usings = array_merge($this->usings, self::get_types($params[0]->getType())); $method->addBody('$data = $?;', [array_values($method->getParameters())[0]->getName()]); + $http_method = 'POST'; } - $method->addBody('$type = ?;', [$this->dapr_type]); - $method->addBody('$id = $this->get_id();'); - $method->addBody('$current_method = ?;', [$method->getName()]); - $method->addBody('$result = $this->client->post('); - $method->addBody(' "/actors/$type/$id/method/$current_method",'); - if (empty($params)) { - $method->addBody(' null);'); - } else { - $method->addBody(' $this->serializer->as_array($data)'); - $method->addBody(');'); + + foreach ($method->getAttributes() as $attribute) { + $http_method = match ($attribute->getName()) { + Get::class => 'GET', + Delete::class => 'DELETE', + Post::class => 'POST', + Put::class => 'PUT', + default => $http_method + }; } + + $method->addBody('$current_method = ?;', [$method->getName()]); + $method->addBody('$http_method = ?;', [$http_method]); + $method->addBody( + '$result = $this->client->invokeActorMethod($http_method, $this->_get_actor_reference(), $current_method, $data \?\? null, ?);', + [$method->getReturnType() ?? 'array'] + ); $return_type = $method->getReturnType() ?? Type::MIXED; if ($return_type !== Type::VOID) { $this->usings = array_merge($this->usings, self::get_types($return_type)); - $method->addBody( - '$result->data = $this->deserializer->detect_from_method((new \ReflectionClass($this))->getMethod(?), $result->data);', - [$method->getName()] - ); - $method->addBody('return $result->data;'); + $method->addBody('return $result;'); } return $method; diff --git a/src/lib/Actors/Generators/GenerateProxy.php b/src/lib/Actors/Generators/GenerateProxy.php index 720e1ad..0cf4954 100644 --- a/src/lib/Actors/Generators/GenerateProxy.php +++ b/src/lib/Actors/Generators/GenerateProxy.php @@ -3,6 +3,7 @@ namespace Dapr\Actors\Generators; use Dapr\Actors\IActor; +use Dapr\Client\DaprClient; use DI\FactoryInterface; use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\Method; @@ -21,8 +22,7 @@ abstract class GenerateProxy implements IGenerateProxy public function __construct( protected string $interface, protected string $dapr_type, - protected FactoryInterface $factory, - protected ContainerInterface $container + protected DaprClient $client ) { } diff --git a/src/lib/Actors/Generators/IGenerateProxy.php b/src/lib/Actors/Generators/IGenerateProxy.php index b02e14c..7f72225 100644 --- a/src/lib/Actors/Generators/IGenerateProxy.php +++ b/src/lib/Actors/Generators/IGenerateProxy.php @@ -3,6 +3,7 @@ namespace Dapr\Actors\Generators; use Dapr\Actors\IActor; +use Dapr\Client\DaprClient; use DI\FactoryInterface; use Psr\Container\ContainerInterface; @@ -18,8 +19,7 @@ interface IGenerateProxy public function __construct( string $interface, string $dapr_type, - FactoryInterface $factory, - ContainerInterface $container + DaprClient $client ); /** diff --git a/src/lib/Actors/Generators/ProxyFactory.php b/src/lib/Actors/Generators/ProxyFactory.php index 4e5dfac..636252a 100644 --- a/src/lib/Actors/Generators/ProxyFactory.php +++ b/src/lib/Actors/Generators/ProxyFactory.php @@ -2,8 +2,8 @@ namespace Dapr\Actors\Generators; +use Dapr\Client\DaprClient; use DI\DependencyException; -use DI\FactoryInterface; use DI\NotFoundException; use InvalidArgumentException; @@ -24,10 +24,9 @@ class ProxyFactory /** * ProxyFactory constructor. * - * @param FactoryInterface $factory * @param int $mode */ - public function __construct(private FactoryInterface $factory, private int $mode) + public function __construct(private int $mode, private DaprClient $client) { } @@ -44,10 +43,10 @@ public function get_generator(string $interface, string $dapr_type): IGeneratePr $params = ['interface' => $interface, 'dapr_type' => $dapr_type]; return match ($this->mode) { - ProxyFactory::DYNAMIC => $this->factory->make(DynamicGenerator::class, $params), - ProxyFactory::GENERATED_CACHED => $this->factory->make(CachedGenerator::class, $params), - ProxyFactory::GENERATED => $this->factory->make(FileGenerator::class, $params), - ProxyFactory::ONLY_EXISTING => $this->factory->make(ExistingOnly::class, $params), + ProxyFactory::DYNAMIC => new DynamicGenerator($interface, $dapr_type, $this->client), + ProxyFactory::GENERATED_CACHED => new CachedGenerator($interface, $dapr_type, $this->client), + ProxyFactory::GENERATED => new FileGenerator($interface, $dapr_type, $this->client), + ProxyFactory::ONLY_EXISTING => new ExistingOnly($interface, $dapr_type, $this->client), default => throw new InvalidArgumentException('mode must be a supported mode'), }; } diff --git a/src/lib/Actors/IActor.php b/src/lib/Actors/IActor.php index e944650..34a2f74 100644 --- a/src/lib/Actors/IActor.php +++ b/src/lib/Actors/IActor.php @@ -44,7 +44,7 @@ function on_deactivation(): void; * * @return bool */ - function delete_timer(string $name, DaprClient $client): bool; + function delete_timer(string $name, ?DaprClient $client = null): bool; /** * Creates a new timer, which lasts until the actor is deactivated. @@ -53,7 +53,7 @@ function delete_timer(string $name, DaprClient $client): bool; * * @return bool */ - function create_timer(Timer $timer, DaprClient $client): bool; + function create_timer(Timer $timer, ?DaprClient $client = null): bool; /** * Deletes a reminder @@ -62,7 +62,7 @@ function create_timer(Timer $timer, DaprClient $client): bool; * * @return bool Whether the deletion was successful */ - function delete_reminder(string $name, DaprClient $client): bool; + function delete_reminder(string $name, ?DaprClient $client = null): bool; /** * Get a reminder by name @@ -71,7 +71,7 @@ function delete_reminder(string $name, DaprClient $client): bool; * * @return Reminder|null Information about the reminder */ - function get_reminder(string $name, DaprClient $client): Reminder|null; + function get_reminder(string $name, ?DaprClient $client = null): Reminder|null; /** * Create a new reminder that will wake up the actor. @@ -80,5 +80,5 @@ function get_reminder(string $name, DaprClient $client): Reminder|null; * * @return bool Whether the reminder was successfully created */ - function create_reminder(Reminder $reminder, DaprClient $client): bool; + function create_reminder(Reminder $reminder, ?DaprClient $client = null): bool; } diff --git a/src/lib/Actors/Internal/Caches/CacheInterface.php b/src/lib/Actors/Internal/Caches/CacheInterface.php index 7410948..d25d9f1 100644 --- a/src/lib/Actors/Internal/Caches/CacheInterface.php +++ b/src/lib/Actors/Internal/Caches/CacheInterface.php @@ -2,6 +2,8 @@ namespace Dapr\Actors\Internal\Caches; +use Dapr\Actors\ActorReference; + /** * Interface CacheInterface * @package Dapr\Actors\Internal\Caches @@ -11,11 +13,10 @@ interface CacheInterface /** * CacheInterface constructor. * - * @param string $dapr_type The dapr type - * @param string $actor_id The actor id + * @param ActorReference $reference * @param string $state_name The name of the state type */ - public function __construct(string $dapr_type, string $actor_id, string $state_name); + public function __construct(ActorReference $reference, string $state_name); /** * Retrieve a key from the cache diff --git a/src/lib/Actors/Internal/Caches/FileCache.php b/src/lib/Actors/Internal/Caches/FileCache.php index 47fbbde..faca98a 100644 --- a/src/lib/Actors/Internal/Caches/FileCache.php +++ b/src/lib/Actors/Internal/Caches/FileCache.php @@ -2,6 +2,7 @@ namespace Dapr\Actors\Internal\Caches; +use Dapr\Actors\ActorReference; use Dapr\State\FileWriter; use phpDocumentor\Reflection\File; @@ -16,15 +17,15 @@ class FileCache extends MemoryCache implements CacheInterface /** * @inheritDoc */ - public function __construct(private string $dapr_type, private string $actor_id, private string $state_name) + public function __construct(private ActorReference $reference, private string $state_name) { - parent::__construct($this->dapr_type, $this->actor_id, $this->state_name); - $base_dir = self::get_base_path($this->dapr_type, $this->actor_id); + parent::__construct($this->reference, $this->state_name); + $base_dir = self::get_base_path($this->reference->get_actor_type(), $this->reference->get_actor_id()); if ( ! file_exists($base_dir)) { mkdir($base_dir, recursive: true); } - $this->state_name = mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '', $this->state_name); - $this->state_name = mb_ereg_replace("([\.]{2,})", '', $this->state_name); + $this->state_name = mb_ereg_replace('([^\w\s\d\-_~,;\[\]\(\).])', '', $this->state_name) . ''; + $this->state_name = mb_ereg_replace('([\.]{2,})', '', $this->state_name) . ''; $this->cache_file = $base_dir.DIRECTORY_SEPARATOR.$this->state_name.'.actor'; $this->unserialize_cache(); } diff --git a/src/lib/Actors/Internal/Caches/MemoryCache.php b/src/lib/Actors/Internal/Caches/MemoryCache.php index 8a0d122..8be783f 100644 --- a/src/lib/Actors/Internal/Caches/MemoryCache.php +++ b/src/lib/Actors/Internal/Caches/MemoryCache.php @@ -2,6 +2,8 @@ namespace Dapr\Actors\Internal\Caches; +use Dapr\Actors\ActorReference; + /** * Class NoCache * @package Dapr\Actors\Internal\Caches @@ -13,7 +15,7 @@ class MemoryCache implements CacheInterface /** * @inheritDoc */ - public function __construct(string $dapr_type, string $actor_id, string $state_name) + public function __construct(ActorReference $reference, string $state_name) { } diff --git a/src/lib/Actors/Reminder.php b/src/lib/Actors/Reminder.php index 636518f..b0707e3 100644 --- a/src/lib/Actors/Reminder.php +++ b/src/lib/Actors/Reminder.php @@ -2,7 +2,11 @@ namespace Dapr\Actors; +use Dapr\Deserialization\Deserializers\IDeserialize; +use Dapr\Deserialization\IDeserializer; use Dapr\Formats; +use Dapr\Serialization\ISerializer; +use Dapr\Serialization\Serializers\ISerialize; use DateInterval; use JetBrains\PhpStorm\ArrayShape; @@ -13,7 +17,7 @@ * * @package Dapr\Actors */ -class Reminder +class Reminder implements ISerialize, IDeserialize { /** * Reminder constructor. @@ -27,7 +31,8 @@ public function __construct( public string $name, public DateInterval $due_time, public mixed $data, - public ?DateInterval $period = null + public ?DateInterval $period = null, + public int $repetitions = -1 ) { } @@ -53,13 +58,40 @@ public static function from_api( ); } + public static function deserialize(mixed $value, IDeserializer $deserializer): mixed + { + return new Reminder( + '', + Formats::from_dapr_interval($value['dueTime']), + json_decode($value['data'] ?? []), + Formats::from_dapr_interval($value['period'] ?? '') + ); + } + #[ArrayShape(['dueTime' => "string", 'period' => "string", 'data' => "false|string"])] public function to_array(): array { return [ 'dueTime' => Formats::normalize_interval($this->due_time), - 'period' => Formats::normalize_interval($this->period), - 'data' => json_encode($this->data), + 'period' => Formats::normalize_interval($this->period), + 'data' => json_encode($this->data), + ]; + } + + public function serialize(mixed $value, ISerializer $serializer): mixed + { + if ($this->repetitions >= 0) { + return [ + 'dueTime' => Formats::normalize_interval($this->due_time), + 'period' => "R{$this->repetitions}/" . $serializer->as_array($this->period), + 'data' => $serializer->as_json($this->data), + ]; + } + + return [ + 'dueTime' => Formats::normalize_interval($this->due_time), + 'period' => Formats::normalize_interval($this->period), + 'data' => $serializer->as_json($this->data) ]; } } diff --git a/src/lib/Actors/Timer.php b/src/lib/Actors/Timer.php index 39216ea..50bf292 100644 --- a/src/lib/Actors/Timer.php +++ b/src/lib/Actors/Timer.php @@ -3,6 +3,8 @@ namespace Dapr\Actors; use Dapr\Formats; +use Dapr\Serialization\ISerializer; +use Dapr\Serialization\Serializers\ISerialize; use DateInterval; use JetBrains\PhpStorm\ArrayShape; @@ -13,7 +15,7 @@ * * @package Dapr\Actors */ -class Timer +class Timer implements ISerialize { public function __construct( public string $name, @@ -24,14 +26,19 @@ public function __construct( ) { } + public function serialize(mixed $value, ISerializer $serializer): mixed + { + return $this->to_array(); + } + #[ArrayShape(['dueTime' => "string", 'period' => "string", 'callback' => "string", 'data' => "array|null"])] public function to_array(): array { return [ - 'dueTime' => Formats::normalize_interval($this->due_time), - 'period' => Formats::normalize_interval($this->period), + 'dueTime' => Formats::normalize_interval($this->due_time), + 'period' => Formats::normalize_interval($this->period), 'callback' => $this->callback, - 'data' => $this->data, + 'data' => $this->data, ]; } } diff --git a/src/lib/App.php b/src/lib/App.php index 810d89c..13cdee1 100644 --- a/src/lib/App.php +++ b/src/lib/App.php @@ -3,6 +3,7 @@ namespace Dapr; use Dapr\Actors\ActorConfig; +use Dapr\Actors\ActorReference; use Dapr\Actors\ActorRuntime; use Dapr\Actors\HealthCheck; use Dapr\Actors\IActor; @@ -226,8 +227,7 @@ function ( ActorRuntime $runtime ) { $runtime->resolve_actor( - $actor_type, - $actor_id, + new ActorReference($actor_id, $actor_type), fn(IActor $actor) => $runtime->deactivate_actor($actor, $actor_type) ); } @@ -247,22 +247,19 @@ function ( $arg = json_decode($request->getBody()->getContents(), true); if ($method_name === 'remind') { $runtime->resolve_actor( - $actor_type, - $actor_id, + new ActorReference($actor_id, $actor_type), fn(IActor $actor) => $actor->remind($reminder_name, Reminder::from_api($reminder_name, $arg)) ); } elseif ($method_name === 'timer') { return $runtime->resolve_actor( - $actor_type, - $actor_id, + new ActorReference($actor_id, $actor_type), fn(IActor $actor) => $response->withBody( $this->serialize_as_stream($runtime->do_method($actor, $arg['callback'], $arg['data'])) ) ); } else { return $runtime->resolve_actor( - $actor_type, - $actor_id, + new ActorReference($actor_id, $actor_type), fn(IActor $actor) => $response->withBody( $this->serialize_as_stream($runtime->do_method($actor, $method_name, $arg)) ) diff --git a/src/lib/Client/DaprClient.php b/src/lib/Client/DaprClient.php index 5e137f1..949dd6d 100644 --- a/src/lib/Client/DaprClient.php +++ b/src/lib/Client/DaprClient.php @@ -2,11 +2,15 @@ namespace Dapr\Client; +use Dapr\Actors\IActorReference; +use Dapr\Actors\Reminder; +use Dapr\Actors\Timer; use Dapr\consistency\Consistency; use Dapr\Deserialization\DeserializationConfig; use Dapr\Deserialization\IDeserializer; use Dapr\Serialization\ISerializer; use Dapr\Serialization\SerializationConfig; +use Dapr\State\Internal\Transaction; use Dapr\State\StateItem; use GuzzleHttp\Promise\PromiseInterface; use Psr\Http\Message\ResponseInterface; @@ -458,6 +462,194 @@ abstract public function getMetadata(): MetadataResponse|null; */ abstract public function shutdown(bool $afterRequest = true): void; + /** + * Invoke an actor method + * + * @param string $httpMethod The HTTP method to use in the invocation + * @param IActorReference $actor The actor reference to invoke + * @param string $method The method of the actor to call + * @param string $as The type to cast the result to using the deserialization config + * @return mixed + */ + abstract public function invokeActorMethod( + string $httpMethod, + IActorReference $actor, + string $method, + mixed $parameter = null, + string $as = 'array' + ): mixed; + + /** + * Invoke an actor method + * + * @param string $httpMethod The HTTP method to use in the invocation + * @param IActorReference $actor The actor reference to invoke + * @param string $method The method of the actor to call + * @param string $as The type to cast the result to using the deserialization config + * @return PromiseInterface + */ + abstract public function invokeActorMethodAsync( + string $httpMethod, + IActorReference $actor, + string $method, + mixed $parameter = null, + string $as = 'array' + ): PromiseInterface; + + /** + * Save a transaction to actor state + * + * @param IActorReference $actor The actor type + * @param Transaction $transaction The transaction to store + * @return bool Whether the state was successfully saved + */ + abstract public function saveActorState(IActorReference $actor, Transaction $transaction): bool; + + /** + * Save a transaction to actor state + * + * @param IActorReference $actor The actor type + * @param Transaction $transaction The transaction to store + * @return PromiseInterface Whether the state was successfully saved + */ + abstract public function saveActorStateAsync(IActorReference $actor, Transaction $transaction): PromiseInterface; + + /** + * Retrieve actor state by key + * + * @param IActorReference $actor The actor reference + * @param string $key The key to retrieve + * @param string $as The type to deserialize to + * @return mixed + */ + abstract public function getActorState(IActorReference $actor, string $key, string $as = 'array'): mixed; + + /** + * Retrieve actor state by key + * + * @param IActorReference $actor The actor reference + * @param string $key The key to retrieve + * @param string $as The type to deserialize to + * @return mixed + */ + abstract public function getActorStateAsync( + IActorReference $actor, + string $key, + string $as = 'array' + ): PromiseInterface; + + /** + * Set up an actor reminder. + * + * @param IActorReference $actor The actor reference + * @param string $reminderName The reminder name to update/create + * @param \DateTimeImmutable|\DateInterval $dueTime When to schedule the reminder for + * @param \DateInterval|int|null $period How often to repeat + * @return bool + */ + abstract public function createActorReminder( + IActorReference $actor, + Reminder $reminder + ): bool; + + /** + * Set up an actor reminder. + * + * @param IActorReference $actor The actor reference + * @param string $reminderName The reminder name to update/create + * @param \DateTimeImmutable|\DateInterval $dueTime When to schedule the reminder for + * @param \DateInterval|int|null $period How often to repeat + * @return PromiseInterface + */ + abstract public function createActorReminderAsync( + IActorReference $actor, + Reminder $reminder + ): PromiseInterface; + + /** + * Get an actor reminder + * + * @param IActorReference $actor + * @param string $name + * @return Reminder + */ + abstract public function getActorReminder(IActorReference $actor, string $name): ?Reminder; + + /** + * Get an actor reminder + * + * @param IActorReference $actor + * @param string $name + * @return PromiseInterface + */ + abstract public function getActorReminderAsync(IActorReference $actor, string $name): PromiseInterface; + + /** + * Delete an actor reminder + * + * @param IActorReference $actor + * @param string $name + * @return bool + */ + abstract public function deleteActorReminder(IActorReference $actor, string $name): bool; + + /** + * Delete an actor reminder + * + * @param IActorReference $actor + * @param string $name + * @return PromiseInterface + */ + abstract public function deleteActorReminderAsync(IActorReference $actor, string $name): PromiseInterface; + + /** + * Create an actor timer + * + * @param IActorReference $actor + * @param string $timerName + * @param \DateInterval|\DateTimeImmutable $dueTime + * @param int|\DateInterval|null $period + * @param string|null $callback + * @return bool + */ + abstract public function createActorTimer( + IActorReference $actor, + Timer $timer + ): bool; + + /** + * Create an actor timer + * + * @param IActorReference $actor + * @param string $timerName + * @param \DateInterval|\DateTimeImmutable $dueTime + * @param int|\DateInterval|null $period + * @param string|null $callback + * @return PromiseInterface + */ + abstract public function createActorTimerAsync( + IActorReference $actor, + Timer $timer + ): PromiseInterface; + + /** + * Delete an actor timer + * + * @param IActorReference $actor + * @param string $name + * @return bool + */ + abstract public function deleteActorTimer(IActorReference $actor, string $name): bool; + + /** + * Delete an actor timer + * + * @param IActorReference $actor + * @param string $name + * @return PromiseInterface + */ + abstract public function deleteActorTimerAsync(IActorReference $actor, string $name): PromiseInterface; + /** * @param string $token * @return null|array{dapr-api-token: string} diff --git a/src/lib/Client/DaprHttpClient.php b/src/lib/Client/DaprHttpClient.php index eca7a6f..e32e1bd 100644 --- a/src/lib/Client/DaprHttpClient.php +++ b/src/lib/Client/DaprHttpClient.php @@ -18,8 +18,13 @@ class DaprHttpClient extends DaprClient use HttpInvokeTrait; use HttpPubSubTrait; use HttpBindingTrait; + use HttpActorTrait; - private Client $httpClient; + protected Client $httpClient; + + protected function getHttpClient(): Client { + return $this->httpClient; + } public function __construct( private string $baseHttpUri, diff --git a/src/lib/Client/HttpActorTrait.php b/src/lib/Client/HttpActorTrait.php new file mode 100644 index 0000000..b3efd19 --- /dev/null +++ b/src/lib/Client/HttpActorTrait.php @@ -0,0 +1,201 @@ +invokeActorMethodAsync($httpMethod, $actor, $method, $parameter, $as)->wait(true); + } + + public function invokeActorMethodAsync( + string $httpMethod, + IActorReference $actor, + string $method, + mixed $parameter = null, + string $as = 'array' + ): PromiseInterface { + $uri = "/v1.0/actors/{$actor->get_actor_type()}/{$actor->get_actor_id()}/method/$method"; + $options = ['body' => $this->serializer->as_json($parameter)]; + $request = match ($httpMethod) { + 'GET' => $this->httpClient->getAsync($uri), + 'POST' => $this->httpClient->postAsync($uri, $options), + 'PUT' => $this->httpClient->putAsync($uri, $options), + 'DELETE' => $this->httpClient->deleteAsync($uri), + default => throw new \InvalidArgumentException( + "$httpMethod is not a supported actor invocation method. Must be GET/POST/PUT/DELETE" + ) + }; + + return $this->handlePromise( + $request, + fn(ResponseInterface $response) => $this->deserializer->from_json($as, $response->getBody()->getContents()) + ); + } + + public function saveActorState(IActorReference $actor, Transaction $transaction): bool + { + return $this->saveActorStateAsync($actor, $transaction)->wait(true); + } + + public function saveActorStateAsync(IActorReference $actor, Transaction $transaction): PromiseInterface + { + if ($transaction->is_empty()) { + return new FulfilledPromise(true); + } + + $options = ['body' => $this->serializer->as_json($transaction->get_transaction())]; + return $this->handlePromise( + $this->httpClient->postAsync( + "/v1.0/actors/{$actor->get_actor_type()}/{$actor->get_actor_id()}/state", + $options + ), + fn(ResponseInterface $response) => $response->getStatusCode() === 204 + ); + } + + public function getActorState(IActorReference $actor, string $key, string $as = 'array'): mixed + { + return $this->getActorStateAsync($actor, $key, $as)->wait(true); + } + + public function getActorStateAsync(IActorReference $actor, string $key, string $as = 'array'): PromiseInterface + { + return $this->handlePromise( + $this->httpClient->getAsync("/v1.0/actors/{$actor->get_actor_type()}/{$actor->get_actor_id()}/state/$key"), + fn(ResponseInterface $response) => match ($response->getStatusCode()) { + KeyResponse::SUCCESS => $this->deserializer->from_json($as, $response->getBody()->getContents()), + KeyResponse::KEY_NOT_FOUND => throw new KeyNotFound(), + } + ); + } + + public function createActorReminder( + IActorReference $actor, + Reminder $reminder + ): bool { + return $this->createActorReminderAsync($actor, $reminder)->wait(true); + } + + public function createActorReminderAsync( + IActorReference $actor, + Reminder $reminder + ): PromiseInterface { + return $this->handlePromise( + $this->httpClient->postAsync( + "/v1.0/actors/{$actor->get_actor_type()}/{$actor->get_actor_id()}/reminders/{$reminder->name}", + [ + 'body' => $this->serializer->as_json($reminder) + ] + ), + fn(ResponseInterface $response) => $response->getStatusCode() === 204 + ); + } + + public function getActorReminder(IActorReference $actor, string $name): ?Reminder + { + return $this->getActorReminderAsync($actor, $name)->wait(true); + } + + public function getActorReminderAsync(IActorReference $actor, string $name): PromiseInterface + { + return $this->handlePromise( + $this->httpClient->getAsync( + "/v1.0/actors/{$actor->get_actor_type()}/{$actor->get_actor_id()}/reminders/$name" + ), + fn(ResponseInterface $response) => $response->getStatusCode() === 200 ? $this->deserializer->from_json( + Reminder::class, + $response->getBody()->getContents() + ) : null + )->then( + function (?Reminder $reminder) use ($name) { + if ($reminder === null) { + return null; + } + $reminder->name = $name; + return $reminder; + } + ); + } + + public function deleteActorReminder(IActorReference $actor, string $name): bool + { + return $this->deleteActorReminderAsync($actor, $name)->wait(true); + } + + public function deleteActorReminderAsync(IActorReference $actor, string $name): PromiseInterface + { + return $this->handlePromise( + $this->httpClient->deleteAsync( + "/v1.0/actors/{$actor->get_actor_type()}/{$actor->get_actor_id()}/reminders/$name" + ), + fn(ResponseInterface $response) => $response->getStatusCode() === 204 + ); + } + + public function createActorTimer( + IActorReference $actor, + Timer $timer + ): bool { + return $this->createActorTimerAsync($actor, $timer)->wait(true); + } + + public function createActorTimerAsync( + IActorReference $actor, + Timer $timer + ): PromiseInterface { + return $this->handlePromise( + $this->httpClient->postAsync( + "/v1.0/actors/{$actor->get_actor_type()}/{$actor->get_actor_id()}/timers/{$timer->name}", + [ + 'body' => $this->serializer->as_json($timer) + ] + ), + fn(ResponseInterface $response) => $response->getStatusCode() === 204 + ); + } + + public function deleteActorTimer(IActorReference $actor, string $name): bool + { + return $this->deleteActorTimerAsync($actor, $name)->wait(true); + } + + public function deleteActorTimerAsync(IActorReference $actor, string $name): PromiseInterface + { + return $this->handlePromise( + $this->httpClient->deleteAsync( + "/v1.0/actors/{$actor->get_actor_type()}/{$actor->get_actor_id()}/timers/$name" + ), + fn(ResponseInterface $response) => $response->getStatusCode() === 204 + ); + } +} diff --git a/src/lib/Client/HttpBindingTrait.php b/src/lib/Client/HttpBindingTrait.php index e00bb7e..8226ea1 100644 --- a/src/lib/Client/HttpBindingTrait.php +++ b/src/lib/Client/HttpBindingTrait.php @@ -17,7 +17,7 @@ trait HttpBindingTrait { public ISerializer $serializer; public IDeserializer $deserializer; - private Client $client; + protected Client $httpClient; /** * @throws DaprException diff --git a/src/lib/Client/HttpInvokeTrait.php b/src/lib/Client/HttpInvokeTrait.php index 15cd86b..e366412 100644 --- a/src/lib/Client/HttpInvokeTrait.php +++ b/src/lib/Client/HttpInvokeTrait.php @@ -19,7 +19,7 @@ trait HttpInvokeTrait public IDeserializer $deserializer; public ISerializer $serializer; - private Client $httpClient; + protected Client $httpClient; /** * @throws DaprException diff --git a/src/lib/Client/HttpPubSubTrait.php b/src/lib/Client/HttpPubSubTrait.php index 040d4e6..10f04e7 100644 --- a/src/lib/Client/HttpPubSubTrait.php +++ b/src/lib/Client/HttpPubSubTrait.php @@ -18,7 +18,7 @@ trait HttpPubSubTrait public ISerializer $serializer; public IDeserializer $deserializer; - private Client $client; + protected Client $client; /** * @throws DaprException diff --git a/src/lib/Client/HttpSecretsTrait.php b/src/lib/Client/HttpSecretsTrait.php index f625771..16f8098 100644 --- a/src/lib/Client/HttpSecretsTrait.php +++ b/src/lib/Client/HttpSecretsTrait.php @@ -18,7 +18,7 @@ trait HttpSecretsTrait public IDeserializer $deserializer; public ISerializer $serializer; - private Client $httpClient; + protected Client $httpClient; public function getSecret(string $storeName, string $key, array $metadata = []): array|null { diff --git a/src/lib/Client/HttpStateTrait.php b/src/lib/Client/HttpStateTrait.php index 1c91999..59e227b 100644 --- a/src/lib/Client/HttpStateTrait.php +++ b/src/lib/Client/HttpStateTrait.php @@ -22,7 +22,7 @@ trait HttpStateTrait public IDeserializer $deserializer; public ISerializer $serializer; - private Client $httpClient; + protected Client $httpClient; public function getState( string $storeName, diff --git a/src/lib/Deserialization/Deserializer.php b/src/lib/Deserialization/Deserializer.php index 8a57cc1..a1c34e7 100644 --- a/src/lib/Deserialization/Deserializer.php +++ b/src/lib/Deserialization/Deserializer.php @@ -88,11 +88,16 @@ public function from_array_of(string $as, array $array): array */ public function from_value(string $as, mixed $value): mixed { + if (str_ends_with($as, '[]')) { + $array_type = substr($as, 0, strpos($as, '[]')); + return $this->from_array_of($array_type, $value); + } + if ($deserializer = $this->get_deserializer($as)) { return $deserializer->deserialize($value, $this); } - if ( ! class_exists($as)) { + if (!class_exists($as)) { return $value; } @@ -180,6 +185,19 @@ private function get_type_from_type(ReflectionType|null|string $type): string return 'mixed'; } + public function detect_type_name_from_property(ReflectionProperty $property): string + { + if ($array_of = $this->is_array_of($property)) { + return $array_of . '[]'; + } + + if ($class_name = $this->is_class($property)) { + return $class_name; + } + + return $this->get_type_from_type($property->getType()); + } + /** * @inheritDoc */ diff --git a/src/lib/Deserialization/IDeserializer.php b/src/lib/Deserialization/IDeserializer.php index 00c30c5..fa7efa4 100644 --- a/src/lib/Deserialization/IDeserializer.php +++ b/src/lib/Deserialization/IDeserializer.php @@ -65,6 +65,14 @@ public function detect_from_parameter(ReflectionParameter $parameter, mixed $dat */ public function detect_from_property(ReflectionProperty $property, mixed $data): mixed; + /** + * Return the detected type from the reflected property + * + * @param ReflectionProperty $property + * @return string + */ + public function detect_type_name_from_property(ReflectionProperty $property): string; + /** * Deserializes based on a method return type (such as when returning from an actor). * diff --git a/src/lib/Formats.php b/src/lib/Formats.php index 104874a..7deec64 100644 --- a/src/lib/Formats.php +++ b/src/lib/Formats.php @@ -43,10 +43,10 @@ public static function normalize_interval(?DateInterval $interval): string public static function coalesce(DateInterval $interval): bool|DateInterval { - $from = new DateTime(); - $to = clone $from; - $to = $to->add($interval); - $diff = $to->diff($from, true); + $from = new DateTime(); + $to = clone $from; + $to = $to->add($interval); + $diff = $to->diff($from, true); $diff->h += $diff->d * self::DAY_IN_HOURS; $diff->d = 0; @@ -65,14 +65,14 @@ public static function from_dapr_interval(string $dapr_interval): ?DateInterval return null; } - $parts = array_combine( + $parts = array_combine( preg_split('/[0-9]/', $dapr_interval, 0, PREG_SPLIT_NO_EMPTY), preg_split('/[a-z]/', $dapr_interval, 0, PREG_SPLIT_NO_EMPTY) ); $interval = ['PT']; - $seconds = 0.0; + $seconds = 0.0; foreach ($parts as $time => $value) { - $seconds += match ($time) { + $seconds += match ($time) { 'ns' => ((float)$value) * self::NANOSECOND_TO_SECOND, 'us', 'µs' => ((float)$value) * self::MICROSECOND_TO_SECOND, 'ms' => ((float)$value) * self::MILLISECOND_TO_SECOND, @@ -80,13 +80,13 @@ public static function from_dapr_interval(string $dapr_interval): ?DateInterval default => 0, }; $interval[] = match ($time) { - 'm' => $value.'M', - 'h' => $value.'H', + 'm' => $value . 'M', + 'h' => $value . 'H', default => null }; } if ($seconds > 0) { - $interval[] = $seconds.'S'; + $interval[] = $seconds . 'S'; } return new DateInterval(implode('', $interval)); diff --git a/src/lib/State/Internal/Transaction.php b/src/lib/State/Internal/Transaction.php index 62739a9..d2aa587 100644 --- a/src/lib/State/Internal/Transaction.php +++ b/src/lib/State/Internal/Transaction.php @@ -51,6 +51,11 @@ function ($a) { ); } + public function is_empty(): bool + { + return empty($this->transaction); + } + /** * Upsert a value in a transaction * @@ -60,10 +65,10 @@ function ($a) { public function upsert(string $key, mixed $value): void { $this->transaction[$key] = [ - 'order' => $this->counter++, + 'order' => $this->counter++, 'operation' => 'upsert', - 'request' => [ - 'key' => $key, + 'request' => [ + 'key' => $key, 'value' => $value, ], ]; @@ -77,9 +82,9 @@ public function upsert(string $key, mixed $value): void public function delete(string $key): void { $this->transaction[$key] = [ - 'order' => $this->counter++, + 'order' => $this->counter++, 'operation' => 'delete', - 'request' => [ + 'request' => [ 'key' => $key, ], ]; diff --git a/src/lib/State/TransactionalState.php b/src/lib/State/TransactionalState.php index 18b793e..9150108 100644 --- a/src/lib/State/TransactionalState.php +++ b/src/lib/State/TransactionalState.php @@ -2,6 +2,7 @@ namespace Dapr\State; +use Dapr\Actors\ActorReference; use Dapr\Actors\Internal\Caches\KeyNotFound; use Dapr\Actors\Internal\Caches\MemoryCache; use Dapr\Client\DaprClient; @@ -41,7 +42,7 @@ public function __construct( $this->stateManager = new StateManager($this->client); $this->reflectionClass = new \ReflectionClass($this); - $this->cache = new MemoryCache('', '', ''); + $this->cache = new MemoryCache(new ActorReference('',''), ''); } /** diff --git a/tests/ActorCacheTest.php b/tests/ActorCacheTest.php index 1555c40..a771127 100644 --- a/tests/ActorCacheTest.php +++ b/tests/ActorCacheTest.php @@ -1,5 +1,6 @@ set_key('test', 'test'); $this->assertSame('test', $cache->get_key('test')); $this->expectException(KeyNotFound::class); @@ -29,7 +30,7 @@ public function assertKeyNotExist(CacheInterface $cache, string $key): void { } public function testReadWriteCacheOnSameInstance() { - $cache = new FileCache('test', '123', 'state-test'); + $cache = new FileCache(new ActorReference('test', '123'), 'state-test'); $this->assertKeyNotExist($cache, 'test'); $cache->set_key('test', 'test'); diff --git a/tests/ActorReferenceTest.php b/tests/ActorReferenceTest.php index 520fea6..19f76e0 100644 --- a/tests/ActorReferenceTest.php +++ b/tests/ActorReferenceTest.php @@ -18,12 +18,12 @@ public function testInvalidInterface() { $this->expectException(ReflectionException::class); $reference = new ActorReference('id', 'type'); - $reference->bind('no_exist_interface', new ProxyFactory($this->container, ProxyFactory::GENERATED)); + $reference->bind('no_exist_interface', new ProxyFactory(ProxyFactory::GENERATED, $this->get_new_client())); } public function testExtractActorFromDynamicProxy() { - $factory = new ProxyFactory($this->container, ProxyFactory::DYNAMIC); + $factory = new ProxyFactory(ProxyFactory::DYNAMIC, $this->get_new_client()); $actor = $factory->get_generator(ITestActor::class, 'TestActor')->get_proxy('id'); $reference = ActorReference::get($actor); $this->assertEquals( @@ -34,7 +34,7 @@ public function testExtractActorFromDynamicProxy() public function testExtractActorFromGeneratedProxy() { - $factory = new ProxyFactory($this->container, ProxyFactory::GENERATED); + $factory = new ProxyFactory(ProxyFactory::GENERATED, $this->get_new_client()); $actor = $factory->get_generator(ITestActor::class, 'TestActor')->get_proxy('id'); $reference = ActorReference::get($actor); $this->assertEquals( @@ -48,7 +48,7 @@ public function testBind() $reference = new ActorReference('id', 'TestActor'); $actor = $reference->bind( ITestActor::class, - new ProxyFactory($this->container, ProxyFactory::GENERATED) + new ProxyFactory(ProxyFactory::GENERATED, $this->get_new_client()) ); $new_reference = ActorReference::get($actor); $this->assertEquals($reference, $new_reference); diff --git a/tests/ActorStateTest.php b/tests/ActorStateTest.php index 360f345..0b286a6 100644 --- a/tests/ActorStateTest.php +++ b/tests/ActorStateTest.php @@ -1,14 +1,19 @@ get_started_state('type', uniqid()); + $stack = $this->get_http_client_stack([]); + $client = $this->get_new_client_with_http($stack->client); + $state = $this->get_started_state('type', uniqid(), $client); $state->save_state(); $this->assertTrue(true); // no exception thrown } @@ -34,10 +41,15 @@ public function testSaveEmptyTransaction() * @return ActorState * @throws ReflectionException */ - private function get_started_state(string $type, string $id): ActorState + private function get_started_state(string $type, string $id, DaprClient $client): ActorState { $state = $this->get_state(); - $this->begin_transaction($state, $type, $id); + $this->begin_transaction( + $state, + new ActorReference($id, $type), + $client, + new MemoryCache(new ActorReference($type, $id), 'test') + ); return $state; } @@ -61,13 +73,17 @@ private function get_state(): ActorState * * @throws ReflectionException */ - private function begin_transaction(ActorState $state, string $type, string $id) - { + private function begin_transaction( + ActorState $state, + ActorReference $reference, + DaprClient $client, + CacheInterface $cache + ) { $reflection = new ReflectionClass($state); $reflection = $reflection->getParentClass(); - $method = $reflection->getMethod('begin_transaction'); + $method = $reflection->getMethod('begin_transaction'); $method->setAccessible(true); - $method->invoke($state, $type, $id); + $method->invoke($state, $reference, $client, $cache); } /** @@ -78,43 +94,47 @@ private function begin_transaction(ActorState $state, string $type, string $id) */ public function testSave() { - $state = $this->get_started_state('actor', 'id'); + $stack = $this->get_http_client_stack([new Response(204), new Response(204)]); + $client = $this->get_new_client_with_http($stack->client); + $state = $this->get_started_state('actor', 'id', $client); $state->state = 'ok'; - $this->get_client()->register_post( - "/actors/actor/id/state", - 204, - '', - [ + $state->save_state(); + $request = $this->get_last_request($stack); + $this->assertRequestUri('/v1.0/actors/actor/id/state', $request); + $this->assertRequestBody( + json_encode( [ - 'operation' => 'upsert', - 'request' => [ - 'key' => 'state', - 'value' => 'ok', + [ + 'operation' => 'upsert', + 'request' => [ + 'key' => 'state', + 'value' => 'ok', + ], ], - ], - ] + ] + ), + $request ); - $state->save_state(); - unset($state->state); - $this->get_client()->register_post( - "/actors/actor/id/state", - 204, - '', - [ + $state->save_state(); + $request = $this->get_last_request($stack); + $this->assertRequestUri('/v1.0/actors/actor/id/state', $request); + $this->assertRequestBody( + json_encode( [ - 'operation' => 'delete', - 'request' => [ - 'key' => 'state', + [ + 'operation' => 'delete', + 'request' => [ + 'key' => 'state', + ], ], - ], - ] + ] + ), + $request ); - - $state->save_state(); } /** @@ -122,7 +142,7 @@ public function testSave() */ public function testSaveInvalidProp() { - $state = $this->get_started_state('type', uniqid()); + $state = $this->get_started_state('type', uniqid(), $this->get_new_client()); $this->expectException(InvalidArgumentException::class); $state->no_prop = true; } @@ -132,17 +152,17 @@ public function testSaveInvalidProp() */ public function testLoadingValue() { - $id = uniqid(); - $state = $this->get_started_state('type', $id); - - $this->get_client()->register_get( - "/actors/type/$id/state/state", - KeyResponse::SUCCESS, - 'hello world' - ); + $stack = $this->get_http_client_stack([new Response(KeyResponse::SUCCESS, body: '"hello world"')]); + $client = $this->get_new_client_with_http($stack->client); + $id = uniqid(); + $state = $this->get_started_state('type', $id, $client); $this->assertSame('hello world', $state->state); $this->assertSame('hello world', $state->state); + + $request = $this->get_last_request($stack); + $this->assertRequestUri("/v1.0/actors/type/$id/state/state", $request); + $this->assertRequestMethod('GET', $request); } /** @@ -150,13 +170,16 @@ public function testLoadingValue() */ public function testLoadingNoValue() { - $id = uniqid(); - $state = $this->get_started_state('type', $id); - - $this->get_client()->register_get("/actors/type/$id/state/state", KeyResponse::KEY_NOT_FOUND, ''); + $stack = $this->get_http_client_stack([new Response(KeyResponse::KEY_NOT_FOUND)]); + $client = $this->get_new_client_with_http($stack->client); + $id = uniqid(); + $state = $this->get_started_state('type', $id, $client); $this->assertSame('initial', $state->state); $this->assertSame('initial', $state->state); + $request = $this->get_last_request($stack); + $this->assertRequestMethod('GET', $request); + $this->assertRequestUri("/v1.0/actors/type/$id/state/state", $request); } /** @@ -164,10 +187,10 @@ public function testLoadingNoValue() */ public function testLoadingNoActor() { - $id = uniqid(); - $state = $this->get_started_state('nope', $id); - - $this->get_client()->register_get("/actors/nope/$id/state/state", KeyResponse::ACTOR_NOT_FOUND, ''); + $stack = $this->get_http_client_stack([new Response(KeyResponse::ACTOR_NOT_FOUND)]); + $client = $this->get_new_client_with_http($stack->client); + $id = uniqid(); + $state = $this->get_started_state('nope', $id, $client); $this->expectException(DaprException::class); @@ -179,18 +202,24 @@ public function testLoadingNoActor() */ public function testIsSet() { - $id = uniqid(); - $state = $this->get_started_state('type', $id); - - $this->get_client()->register_get("/actors/type/$id/state/state", KeyResponse::SUCCESS, 'test'); + $stack = $this->get_http_client_stack( + [new Response(KeyResponse::SUCCESS, body: '"test"'), new Response(KeyResponse::SUCCESS, body: 'null')] + ); + $client = $this->get_new_client_with_http($stack->client); + $id = uniqid(); + $state = $this->get_started_state('type', $id, $client); $this->assertTrue(isset($state->state)); $this->assertTrue(isset($state->state)); + $request = $this->get_last_request($stack); + $this->assertRequestUri("/v1.0/actors/type/$id/state/state", $request); + $this->assertRequestMethod('GET', $request); $state->roll_back(); - $this->get_client()->register_get("/actors/type/$id/state/state", KeyResponse::SUCCESS, null); - $this->assertFalse(isset($state->state)); + $request = $this->get_last_request($stack); + $this->assertRequestMethod('GET', $request); + $this->assertRequestUri("/v1.0/actors/type/$id/state/state", $request); } } diff --git a/tests/ActorTest.php b/tests/ActorTest.php index 837343b..180214c 100644 --- a/tests/ActorTest.php +++ b/tests/ActorTest.php @@ -1,6 +1,7 @@ register_actor('TestActor', ActorClass::class); - $this->assertState( + $assert = $this->assertState( [ ['upsert' => ['value', 'new value']], ], $id ); $runtime = $this->container->get(ActorRuntime::class); - $result = $runtime->resolve_actor( - 'TestActor', - $id, + $result = $runtime->resolve_actor( + new ActorReference( + $id, + 'TestActor' + ), fn($actor) => $runtime->do_method($actor, 'a_function', 'new value') ); $this->assertSame(['new value'], $result); + $assert(); } /** @@ -68,10 +73,10 @@ public function testActorInvoke() private function register_actor(string $name, ?string $implementation = null) { if ($implementation === null) { - $reflection = new ReflectionClass($name); - $attr = $reflection->getAttributes(DaprType::class)[0]; + $reflection = new ReflectionClass($name); + $attr = $reflection->getAttributes(DaprType::class)[0]; $implementation = $name; - $name = $attr->newInstance()->type; + $name = $attr->newInstance()->type; } $config = [$name => $implementation]; $this->container->set( @@ -101,14 +106,25 @@ private function assertState($transactions, $id) [$key, $value] = $transform; $return[] = [ 'operation' => $operation, - 'request' => [ - 'key' => $key, + 'request' => [ + 'key' => $key, 'value' => $value, ], ]; } } - $this->get_client()->register_post("/actors/TestActor/$id/state", 201, [], $return); + $stack = $this->get_http_client_stack( + [ + new Response(201) + ] + ); + $client = $this->get_new_client_with_http($stack->client); + $this->container->set(\Dapr\Client\DaprClient::class, $client); + return function () use ($stack, $id, $return) { + $request = $this->get_last_request($stack); + $this->assertRequestUri("/v1.0/actors/TestActor/$id/state", $request); + $this->assertRequestBody(json_encode($return), $request); + }; } /** @@ -122,34 +138,34 @@ public function testActorRuntime() { $id = uniqid(); $this->register_actor(ActorClass::class); - $this->assertState( + $assert = $this->assertState( [ ['upsert' => ['value', 'new value']], ], $id ); $runtime = $this->container->get(ActorRuntime::class); - $result = $runtime->resolve_actor( - 'TestActor', - $id, + $result = $runtime->resolve_actor( + new ActorReference($id, 'TestActor'), fn($actor) => $runtime->do_method($actor, 'a_function', 'new value') ); $this->assertSame(['new value'], $result); - $runtime->resolve_actor('TestActor', $id, fn($actor) => $runtime->deactivate_actor($actor, 'TestActor')); + $runtime->resolve_actor(new ActorReference($id, 'TestActor'), fn($actor) => $runtime->deactivate_actor($actor, 'TestActor')); + $assert(); } #[ArrayShape([ - 'Dynamic Mode' => "array", + 'Dynamic Mode' => "array", 'Generated Mode' => "array", - 'Cached Mode' => "array", - 'Only Existing' => "array", + 'Cached Mode' => "array", + 'Only Existing' => "array", ])] public function getModes(): array { return [ - 'Dynamic Mode' => [ProxyFactory::DYNAMIC], + 'Dynamic Mode' => [ProxyFactory::DYNAMIC], 'Generated Mode' => [ProxyFactory::GENERATED], - 'Cached Mode' => [ProxyFactory::GENERATED_CACHED], - 'Only Existing' => [ProxyFactory::ONLY_EXISTING], + 'Cached Mode' => [ProxyFactory::GENERATED_CACHED], + 'Only Existing' => [ProxyFactory::ONLY_EXISTING], ]; } @@ -166,89 +182,121 @@ public function testActorProxy(int $mode) { $id = uniqid(); - $cache_dir = sys_get_temp_dir().DIRECTORY_SEPARATOR.uniqid('dapr_test_cache_').DIRECTORY_SEPARATOR; + $cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('dapr_test_cache_') . DIRECTORY_SEPARATOR; $this->createBuilder([CachedGenerator::class => autowire()->method('set_cache_dir', $cache_dir)]); $type = uniqid('TestActor_'); - if($mode === ProxyFactory::ONLY_EXISTING) { + if ($mode === ProxyFactory::ONLY_EXISTING) { // make sure the actor has been loaded - $this->get_actor_generator(ProxyFactory::GENERATED_CACHED, ITestActor::class, $type)->get_proxy($id); + $this->get_actor_generator( + ProxyFactory::GENERATED_CACHED, + ITestActor::class, + $type, + $this->get_new_client() + )->get_proxy($id); } + $stack = $this->get_http_client_stack( + [ + new Response( + 200, body: json_encode( + [ + 'dueTime' => '1s', + 'period' => '10s', + 'data' => '[0]' + ] + ) + ), + new Response(200), + new Response(200), + new Response(204), + new Response(204), + new Response(200, body: '["true"]'), + new Response(200, body: '["true"]'), + ] + ); + $client = $this->get_new_client_with_http($stack->client); + + $get_last_request = function () use ($stack) { + static $id = 0; + return $stack->history[$id++]['request']; + }; /** * @var ITestActor|IActor $proxy */ - $proxy = $this->get_actor_generator($mode, ITestActor::class, $type)->get_proxy($id); - + $proxy = $this->get_actor_generator($mode, ITestActor::class, $type, $client)->get_proxy($id); $this->assertSame($id, $proxy->get_id()); - $this->get_client()->register_get( - "/actors/$type/$id/reminders/reminder", - 200, - [ - "dueTime" => '1s', - 'period' => '10s', - 'data' => "[0]", - ] - ); - $reminder = $proxy->get_reminder('reminder', $this->get_client()); - $this->assertSame(1, $reminder->due_time->s); - $this->assertSame(10, $reminder->period->s); - $this->assertSame([0], $reminder->data); - $this->get_client()->register_post( - "/actors/$type/$id/timers/timer", - 200, - [], - [ - 'dueTime' => '0h0m1s0us', - 'period' => '0h0m1s0us', - 'callback' => 'callback', - 'data' => null, - ] + $reminder = $proxy->get_reminder('reminder'); + $this->assertEquals( + new Reminder('reminder', new DateInterval('PT1S'), [0], new DateInterval('PT10S')), + $reminder ); + $request = $get_last_request(); + $this->assertRequestBody('', $request); + $this->assertRequestUri("/v1.0/actors/{$type}/{$id}/reminders/reminder", $request); + $this->assertRequestMethod('GET', $request); + $proxy->create_timer( - new Timer('timer', new DateInterval('PT1S'), new DateInterval('PT1S'), 'callback'), - $this->get_client() + new Timer('timer', new DateInterval('PT1S'), new DateInterval('PT1S'), 'callback') ); - - $this->get_client()->register_post( - "/actors/$type/$id/reminders/reminder", - 200, - [], - [ - 'dueTime' => '0h0m1s0us', - 'period' => '0h0m1s0us', - 'data' => 'null', - ] + $request = $get_last_request(); + $this->assertRequestBody( + json_encode( + [ + 'dueTime' => '0h0m1s0us', + 'period' => '0h0m1s0us', + 'callback' => 'callback', + 'data' => null + ] + ), + $request ); + $this->assertRequestUri("/v1.0/actors/{$type}/{$id}/timers/timer", $request); + $this->assertRequestMethod('POST', $request); + $proxy->create_reminder( new Reminder( - 'reminder', new DateInterval('PT1S'), data: null, period: new DateInterval('PT1S') + 'reminder', new DateInterval('PT1S'), data: null, period: new DateInterval('PT1S'), repetitions: 4 + ) + ); + $request = $get_last_request(); + $this->assertRequestBody( + json_encode( + [ + 'dueTime' => '0h0m1s0us', + 'period' => 'R4/PT1S', + 'data' => 'null' + ] ), - $this->get_client() + $request ); + $this->assertRequestUri("/v1.0/actors/{$type}/{$id}/reminders/reminder", $request); + $this->assertRequestMethod('POST', $request); - $this->get_client()->register_delete("/actors/$type/$id/timers/timer", 204); - $proxy->delete_timer('timer', $this->get_client()); + $proxy->delete_timer('timer'); + $request = $get_last_request(); + $this->assertRequestUri("/v1.0/actors/$type/$id/timers/timer", $request); + $this->assertRequestMethod('DELETE', $request); - $this->get_client()->register_delete("/actors/$type/$id/reminders/reminder", 204); - $proxy->delete_reminder('reminder', $this->get_client()); + $proxy->delete_reminder('reminder'); + $request = $get_last_request(); + $this->assertRequestUri("/v1.0/actors/$type/$id/reminders/reminder", $request); + $this->assertRequestMethod('DELETE', $request); - $this->get_client()->register_post( - path: "/actors/$type/$id/method/a_function", - code: 200, - response_data: ['true'], - expected_request: null - ); - $proxy->a_function(null); + $result = $proxy->a_function(null); + $this->assertSame(['true'], $result); + $request = $get_last_request(); + $this->assertRequestUri("/v1.0/actors/$type/$id/method/a_function", $request); + $this->assertRequestBody('null', $request); + $this->assertRequestMethod('POST', $request); - $this->get_client()->register_post( - path: "/actors/$type/$id/method/a_function", - code: 200, - response_data: ['true'], - expected_request: "ok" - ); - $proxy->a_function('ok'); + $result = $proxy->a_function('ok'); + $this->assertSame(['true'], $result); + $request = $get_last_request(); + $this->assertRequestUri("/v1.0/actors/$type/$id/method/a_function", $request); + $this->assertRequestMethod('POST', $request); + $this->assertRequestBody('"ok"', $request); } /** @@ -260,9 +308,13 @@ public function testActorProxy(int $mode) * @throws DependencyException * @throws NotFoundException */ - private function get_actor_generator(int $mode, string $interface, string $type): IGenerateProxy - { - $factory = new ProxyFactory($this->container, $mode); + private function get_actor_generator( + int $mode, + string $interface, + string $type, + \Dapr\Client\DaprClient $client + ): IGenerateProxy { + $factory = new ProxyFactory($mode, $client); return $factory->get_generator($interface, $type); } @@ -281,7 +333,9 @@ public function testCannotManuallyActivate($mode) /** * @var ITestActor $proxy */ - $proxy = $this->get_actor_generator($mode, ITestActor::class, 'TestActor')->get_proxy($id); + $proxy = $this->get_actor_generator($mode, ITestActor::class, 'TestActor', $this->get_new_client())->get_proxy( + $id + ); $this->expectException(LogicException::class); $proxy->on_activation(); } @@ -300,7 +354,9 @@ public function testCannotManuallyDeactivate($mode) /** * @var ITestActor $proxy */ - $proxy = $this->get_actor_generator($mode, ITestActor::class, 'TestActor')->get_proxy($id); + $proxy = $this->get_actor_generator($mode, ITestActor::class, 'TestActor', $this->get_new_client())->get_proxy( + $id + ); $this->expectException(LogicException::class); $proxy->on_deactivation(); } @@ -320,7 +376,9 @@ public function testCannotManuallyRemind($mode) /** * @var ITestActor|IActor $proxy */ - $proxy = $this->get_actor_generator($mode, ITestActor::class, 'TestActor')->get_proxy($id); + $proxy = $this->get_actor_generator($mode, ITestActor::class, 'TestActor', $this->get_new_client())->get_proxy( + $id + ); $this->expectException(LogicException::class); $proxy->remind('', new Reminder('', new DateInterval('PT10S'), '')); } @@ -331,19 +389,21 @@ public function testCannotManuallyRemind($mode) */ public function testCachedGeneratorGenerates() { - $cache_dir = sys_get_temp_dir().DIRECTORY_SEPARATOR.uniqid().DIRECTORY_SEPARATOR; - $this->createBuilder([CachedGenerator::class => autowire()->method('set_cache_dir', $cache_dir)]); - $cache = $cache_dir.'/dapr_proxy_GCached'; + $cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'dapr-proxy-cache' . DIRECTORY_SEPARATOR; + $cache = $cache_dir . '/dapr_proxy_GCached'; if (file_exists($cache)) { unlink($cache); - rmdir($cache_dir); } $this->assertFalse(file_exists($cache), 'cache should be deleted'); - $proxy = $this->get_actor_generator(ProxyFactory::GENERATED_CACHED, ITestActor::class, 'GCached'); + $proxy = $this->get_actor_generator( + ProxyFactory::GENERATED_CACHED, + ITestActor::class, + 'GCached', + $this->get_new_client() + ); $proxy->get_proxy('hi'); $this->assertTrue(file_exists($cache), 'cache should exist'); unlink($cache); - rmdir($cache_dir); } /** @@ -356,11 +416,11 @@ public function testCachedGeneratorGenerates() public function testGeneratedClassIsCorrect() { $generated_class = (string)FileGenerator::generate(ITestActor::class, $this->container); - $take_snapshot = false; + $take_snapshot = false; if ($take_snapshot) { - FileWriter::write(__DIR__.'/Fixtures/GeneratedProxy.php', $generated_class); + FileWriter::write(__DIR__ . '/Fixtures/GeneratedProxy.php', $generated_class); } - $expected_proxy = file_get_contents(__DIR__.'/Fixtures/GeneratedProxy.php'); + $expected_proxy = file_get_contents(__DIR__ . '/Fixtures/GeneratedProxy.php'); $this->assertSame($expected_proxy, $generated_class); } } diff --git a/tests/DaprTests.php b/tests/DaprTests.php index 8b436aa..841a027 100644 --- a/tests/DaprTests.php +++ b/tests/DaprTests.php @@ -133,6 +133,17 @@ protected function get_http_client_stack( return $container; } + protected function get_last_request(MockedHttpClientContainer $stack): \GuzzleHttp\Psr7\Request { + static $map = null; + if($map === null) { + $map = new WeakMap(); + } + $current = $map[$stack] ?? 0; + $request = $stack->history[$current++]['request']; + $map[$stack] = $current; + return $request; + } + protected function deserialize(string $json) { return json_decode($json, true); diff --git a/tests/Fixtures/GeneratedProxy.php b/tests/Fixtures/GeneratedProxy.php index acfc295..7b2a9f0 100644 --- a/tests/Fixtures/GeneratedProxy.php +++ b/tests/Fixtures/GeneratedProxy.php @@ -6,46 +6,39 @@ namespace Dapr\Proxies; +use Dapr\Actors\ActorReference; use Dapr\Actors\ActorTrait; use Dapr\Actors\Attributes\DaprType; use Dapr\Actors\IActor; -use Dapr\DaprClient; +use Dapr\Client\DaprClient; #[DaprType('TestActor')] -class dapr_proxy_TestActor implements \Fixtures\ITestActor, IActor +final class dapr_proxy_TestActor implements \Fixtures\ITestActor, IActor { use ActorTrait; public string $id; - public $DAPR_TYPE = 'TestActor'; + public string $DAPR_TYPE = 'TestActor'; + private ActorReference $reference; #[\Dapr\Deserialization\Attributes\ArrayOf('string')] public function a_function(#[\Dapr\Deserialization\Attributes\AsClass('SimpleObject')] $value): array { $data = $value; - $type = 'TestActor'; - $id = $this->get_id(); $current_method = 'a_function'; - $result = $this->client->post( - "/actors/$type/$id/method/$current_method", - $this->serializer->as_array($data) - ); - $result->data = $this->deserializer->detect_from_method((new \ReflectionClass($this))->getMethod('a_function'), $result->data); - return $result->data; + $http_method = 'POST'; + $result = $this->client->invokeActorMethod($http_method, $this->_get_actor_reference(), $current_method, $data ?? null, 'array'); + return $result; } public function empty_func() { - $type = 'TestActor'; - $id = $this->get_id(); $current_method = 'empty_func'; - $result = $this->client->post( - "/actors/$type/$id/method/$current_method", - null); - $result->data = $this->deserializer->detect_from_method((new \ReflectionClass($this))->getMethod('empty_func'), $result->data); - return $result->data; + $http_method = 'GET'; + $result = $this->client->invokeActorMethod($http_method, $this->_get_actor_reference(), $current_method, $data ?? null, 'array'); + return $result; } @@ -85,10 +78,13 @@ public function on_deactivation(): void } - public function __construct( - private DaprClient $client, - private \Dapr\Serialization\ISerializer $serializer, - private \Dapr\Deserialization\IDeserializer $deserializer, - ) { + public function __construct(private DaprClient $client) + { + } + + + private function _get_actor_reference(): ActorReference + { + return $this->reference ??= new ActorReference($this->id, $this->DAPR_TYPE); } }