This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Virtual Actors

How to build actors

If you’re new to the actor pattern, the best place to learn about the actor pattern is in the Actor Overview.

In the PHP SDK, there are two sides to an actor, the Client, and the Actor (aka, the Runtime). As a client of an actor, you’ll interact with a remote actor via the ActorProxy class. This class generates a proxy class on-the-fly using one of several configured strategies.

When writing an actor, state can be managed for you. You can hook into the actor lifecycle, and define reminders and timers. This gives you considerable power for handling all types of problems that the actor pattern is suited for.

The Actor Proxy

Whenever you want to communicate with an actor, you’ll need to get a proxy object to do so. The proxy is responsible for serializing your request, deserializing the response, and returning it to you, all while obeying the contract defined by the specified interface.

In order to create the proxy, you’ll first need an interface to define how and what you send and receive from an actor. For example, if you want to communicate with a counting actor that solely keeps track of counts, you might define the interface as follows:

<?php
#[\Dapr\Actors\Attributes\DaprType('Counter')]
interface ICount {
    function increment(int $amount = 1): void;
    function get_count(): int;
}

It’s a good idea to put this interface in a shared library that the actor and clients can both access (if both are written in PHP). The DaprType attribute tells the DaprClient the name of the actor to send to. It should match the implementation’s DaprType, though you can override the type if needed.

<?php
$app->run(function(\Dapr\Actors\ActorProxy $actorProxy) {
    $actor = $actorProxy->get(ICount::class, 'actor-id');
    $actor->increment(10);
});

Writing Actors

To create an actor, you need to implement the interface you defined earlier and also add the DaprType attribute. All actors must implement IActor, however there’s an Actor base class that implements the boilerplate making your implementation much simpler.

Here’s the counter actor:

<?php
#[\Dapr\Actors\Attributes\DaprType('Count')]
class Counter extends \Dapr\Actors\Actor implements ICount {
    function __construct(string $id, private CountState $state) {
        parent::__construct($id);
    }
    
    function increment(int $amount = 1): void {
        $this->state->count += $amount;
    }
    
    function get_count(): int {
        return $this->state->count;
    }
}

The most important bit is the constructor. It takes at least one argument with the name of id which is the id of the actor. Any additional arguments are injected by the DI container, including any ActorState you want to use.

Actor Lifecycle

An actor is instantiated via the constructor on every request targeting that actor type. You can use it to calculate ephemeral state or handle any kind of request-specific startup you require, such as setting up other clients or connections.

After the actor is instantiated, the on_activation() method may be called. The on_activation() method is called any time the actor “wakes up” or when it is created for the first time. It is not called on every request.

Next, the actor method is called. This may be from a timer, reminder, or from a client. You may perform any work that needs to be done and/or throw an exception.

Finally, the result of the work is returned to the caller. After some time (depending on how you’ve configured the service), the actor will be deactivated and on_deactivation() will be called. This may not be called if the host dies, daprd crashes, or some other error occurs which prevents it from being called successfully.

Actor State

Actor state is a “Plain Old PHP Object” (POPO) that extends ActorState. The ActorState base class provides a couple of useful methods. Here’s an example implementation:

<?php
class CountState extends \Dapr\Actors\ActorState {
    public int $count = 0;
}

Registering an Actor

Dapr expects to know what actors a service may host at startup. You need to add it to the configuration:

If you want to take advantage of pre-compiled dependency injection, you need to use a factory:

<?php
// in config.php

return [
    'dapr.actors' => fn() => [Counter::class],
];

All that is required to start the app:

<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = \Dapr\App::create(
    configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php')->enableCompilation(__DIR__)
);
$app->start();
<?php
// in config.php

return [
    'dapr.actors' => [Counter::class]
];

All that is required to start the app:

<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php'));
$app->start();

1 - Production Reference: Actors

Running PHP actors in production

Proxy modes

There are four different modes actor proxies are handled. Each mode presents different trade-offs that you’ll need to weigh during development and in production.

<?php
\Dapr\Actors\Generators\ProxyFactory::GENERATED;
\Dapr\Actors\Generators\ProxyFactory::GENERATED_CACHED;
\Dapr\Actors\Generators\ProxyFactory::ONLY_EXISTING;
\Dapr\Actors\Generators\ProxyFactory::DYNAMIC;

It can be set with dapr.actors.proxy.generation configuration key.

This is the default mode. In this mode, a class is generated and eval’d on every request. It’s mostly for development and shouldn’t be used in production.

This is the same as ProxyModes::GENERATED except the class is stored in a tmp file so it doesn’t need to be regenerated on every request. It doesn’t know when to update the cached class, so using it in development is discouraged but is offered for when manually generating the files isn’t possible.

In this mode, an exception is thrown if the proxy class doesn’t exist. This is useful for when you don’t want to generate code in production. You’ll have to make sure the class is generated and pre-/autoloaded.

Generating proxies

You can create a composer script to generate proxies on demand to take advantage of the ONLY_EXISTING mode.

Create a ProxyCompiler.php

<?php

class ProxyCompiler {
    private const PROXIES = [
        MyActorInterface::class,
        MyOtherActorInterface::class,
    ];
    
    private const PROXY_LOCATION = __DIR__.'/proxies/';
    
    public static function compile() {
        try {
            $app = \Dapr\App::create();
            foreach(self::PROXIES as $interface) {
                $output = $app->run(function(\DI\FactoryInterface $factory) use ($interface) {
                    return \Dapr\Actors\Generators\FileGenerator::generate($interface, $factory);
                });
                $reflection = new ReflectionClass($interface);
                $dapr_type = $reflection->getAttributes(\Dapr\Actors\Attributes\DaprType::class)[0]->newInstance()->type;
                $filename = 'dapr_proxy_'.$dapr_type.'.php';
                file_put_contents(self::PROXY_LOCATION.$filename, $output);
                echo "Compiled: $interface";
            }
        } catch (Exception $ex) {
            echo "Failed to generate proxy for $interface\n{$ex->getMessage()} on line {$ex->getLine()} in {$ex->getFile()}\n";
        }
    }
}

Then add a psr-4 autoloader for the generated proxies and a script in composer.json:

{
  "autoload": {
    "psr-4": {
      "Dapr\\Proxies\\": "path/to/proxies"
    }
  },
  "scripts": {
    "compile-proxies": "ProxyCompiler::compile"
  }
}

And finally, configure dapr to only use the generated proxies:

<?php
// in config.php

return [
    'dapr.actors.proxy.generation' => ProxyFactory::ONLY_EXISTING,
];

In this mode, the proxy satisfies the interface contract, however, it does not actually implement the interface itself (meaning instanceof will be false). This mode takes advantage of a few quirks in PHP to work and exists for cases where code cannot be eval’d or generated.

Requests

Creating an actor proxy is very inexpensive for any mode. There are no requests made when creating an actor proxy object.

When you call a method on a proxy object, only methods that you implemented are serviced by your actor implementation. get_id() is handled locally, and get_reminder(), delete_reminder(), etc. are handled by the daprd.

Actor implementation

Every actor implementation in PHP must implement \Dapr\Actors\IActor and use the \Dapr\Actors\ActorTrait trait. This allows for fast reflection and some shortcuts. Using the \Dapr\Actors\Actor abstract base class does this for you, but if you need to override the default behavior, you can do so by implementing the interface and using the trait.

Activation and deactivation

When an actor activates, a token file is written to a temporary directory (by default this is in '/tmp/dapr_' + sha256(concat(Dapr type, id)) in linux and '%temp%/dapr_' + sha256(concat(Dapr type, id)) on Windows). This is persisted until the actor deactivates, or the host shuts down. This allows for on_activation to be called once and only once when Dapr activates the actor on the host.

Performance

Actor method invocation is very fast on a production setup with php-fpm and nginx, or IIS on Windows. Even though the actor is constructed on every request, actor state keys are only loaded on-demand and not during each request. However, there is some overhead in loading each key individually. This can be mitigated by storing an array of data in state, trading some usability for speed. It is not recommended doing this from the start, but as an optimization when needed.

Versioning state

The names of the variables in the ActorState object directly correspond to key names in the store. This means that if you change the type or name of a variable, you may run into errors. To get around this, you may need to version your state object. In order to do this, you’ll need to override how state is loaded and stored. There are many ways to approach this, one such solution might be something like this:

<?php

class VersionedState extends \Dapr\Actors\ActorState {
    /**
     * @var int The current version of the state in the store. We give a default value of the current version. 
     * However, it may be in the store with a different value. 
     */
    public int $state_version = self::VERSION;
    
    /**
     * @var int The current version of the data
     */
    private const VERSION = 3;
    
    /**
     * Call when your actor activates.
     */
    public function upgrade() {
        if($this->state_version < self::VERSION) {
            $value = parent::__get($this->get_versioned_key('key', $this->state_version));
            // update the value after updating the data structure
            parent::__set($this->get_versioned_key('key', self::VERSION), $value);
            $this->state_version = self::VERSION;
            $this->save_state();
        }
    }
    
    // if you upgrade all keys as needed in the method above, you don't need to walk the previous
    // keys when loading/saving and you can just get the current version of the key.
    
    private function get_previous_version(int $version): int {
        return $this->has_previous_version($version) ? $version - 1 : $version;
    }
    
    private function has_previous_version(int $version): bool {
        return $version >= 0;
    }
    
    private function walk_versions(int $version, callable $callback, callable $predicate): mixed {
        $value = $callback($version);
        if($predicate($value) || !$this->has_previous_version($version)) {
            return $value;
        }
        return $this->walk_versions($this->get_previous_version($version), $callback, $predicate);
    }
    
    private function get_versioned_key(string $key, int $version) {
        return $this->has_previous_version($version) ? $version.$key : $key;
    }
    
    public function __get(string $key): mixed {
        return $this->walk_versions(
            self::VERSION, 
            fn($version) => parent::__get($this->get_versioned_key($key, $version)),
            fn($value) => isset($value)
        );
    }
    
    public function __isset(string $key): bool {
        return $this->walk_versions(
            self::VERSION,
            fn($version) => parent::__isset($this->get_versioned_key($key, $version)),
            fn($isset) => $isset
        );
    }
    
    public function __set(string $key,mixed $value): void {
        // optional: you can unset previous versions of the key
        parent::__set($this->get_versioned_key($key, self::VERSION), $value);
    }
    
    public function __unset(string $key) : void {
        // unset this version and all previous versions
        $this->walk_versions(
            self::VERSION, 
            fn($version) => parent::__unset($this->get_versioned_key($key, $version)), 
            fn() => false
        );
    }
}

There’s a lot to be optimized, and it wouldn’t be a good idea to use this verbatim in production, but you can get the gist of how it would work. A lot of it will depend on your use case which is why there’s not something like this in the SDK. For instance, in this example implementation, the previous value is kept for where there may be a bug during an upgrade; keeping the previous value allows for running the upgrade again, but you may wish to delete the previous value.