DEV Community

Cover image for PHP + Actors == Dapr
Rob Landers
Rob Landers

Posted on

1

PHP + Actors == Dapr

Disclosure: I'm the PHP SDK maintainer

PHP and actors?

wait-what.gif

Dapr is a portable, event-driven runtime that makes it easy for any developer to build resilient, stateless and stateful applications that run on the cloud and edge and embraces the diversity of languages and developer frameworks.

Dapr (Distributed Application Runtime) supports PHP, and in my humble opinion, actors are the coolest part about Dapr. Dapr implements the virtual actor pattern. The virtual actor pattern essentially (and this is greatly simplified!) means that any instance of an actor will be "single-threaded" and doesn't exist until it's called. The single-threaded bit sounds exactly like what I'm used to with PHP, and the "doesn't exist until it's called" sounds like something I'd read in the PHP docs. 🤣

OK, it's a bit more than that, and I highly suggest reading Dapr's documentation about it to get a better overview. If you're curious what these actors look like in PHP, stick around.

Setting up the project

With the php-sdk, you create a new project with composer init and run composer require dapr/php-sdk to install it.

For the project, you'll create a simple actor that counts and call it Count, with two methods: a method to get the current count and increment the count by a given amount.

State and stuff

The actor's state is pretty straightforward. Check it out:

<?php
namespace Example\Actor;
use Dapr\Actors\ActorState;
class CountState extends ActorState
{
public int $count = 0;
}
view raw CountState.php hosted with ❤ by GitHub

As you can see, the state has a single key: $count that is an integer with a default value of 0. You only have to extend the ActorState abstract class to use it in an actor (to provide a transaction). That's all you need!

Creating the interface

If you want to use a PHP client, you'll need to define an interface for the actor:

<?php
namespace Example\Interfaces;
use Dapr\Actors\Attributes\DaprType;
#[DaprType('Count')]
interface CountInterface
{
public function getCount(): int;
public function incrementAndGet(int $amount = 1): int;
}
view raw CountInterface.php hosted with ❤ by GitHub

You can see the two methods and an attribute. The #[DaprType()] attribute specifies what the name of the actor is according to Dapr, which need not match what we call it in our code.

Implementing the interface

And now, to implement the interface, it's as simple as you'd expect:

<?php
namespace Example\Actor;
use Dapr\Actors\Actor;
use Dapr\Actors\Attributes\DaprType;
use Example\Interfaces\CountInterface;
#[DaprType('Count')]
class CountActor extends Actor implements CountInterface
{
public function __construct(string $id, private CountState $state)
{
parent::__construct($id);
}
public function getCount(): int
{
return $this->state->count;
}
public function incrementAndGet(int $amount = 1): int
{
return $this->state->count += $amount;
}
}
view raw CountActor.php hosted with ❤ by GitHub

Here you can see the familiar #[DaprType()] attribute, a constructor that takes the actor's id and its private state. From there, the actual implementation is straightforward as we're returning the state and/or incrementing it.

Testing

Of course, the code only looks simple. We should verify that there aren't any shenanigans and it works exactly as you expect. To do that, install phpunit with composer require --dev phpunit/phpunit and write up some unit tests:

<?php
require_once __DIR__.'/../vendor/autoload.php';
use Example\Actor\CountActor;
use Example\Actor\CountState;
class CountTest extends \PHPUnit\Framework\TestCase {
public CountState $state;
public CountActor $actor;
public function setUp(): void
{
parent::setUp();
$container = new \DI\Container();
$this->state = new CountState($container, $container);
$this->actor = new CountActor(uniqid(), $this->state);
}
public function testInitialCount() {
$this->assertSame(0, $this->actor->getCount(), 'initial value should be 0');
}
public function testGetCount() {
$this->state->count = 4;
$this->assertSame(4, $this->actor->getCount(), 'it should not change the count');
}
public function testIncrementFromZero() {
$this->assertSame(1, $this->actor->incrementAndGet(), 'increment by 1 should return 1');
$this->assertSame(1, $this->actor->getCount(), 'the count should not change');
}
public function testIncrementAmount() {
$this->assertSame(4, $this->actor->incrementAndGet(4), 'incrementing by 4 should result in 4 + 0');
$this->assertSame(4, $this->actor->getCount(), 'the count should not change');
}
}
view raw CountTest.php hosted with ❤ by GitHub

None of the underlying framework is initialized for unit tests, so you can assert that given a starting state, the resulting state is what you expect.

You can now test this with phpunit, which you can add as a script so that testing is as easy as composer test.

Tying it all together

Now that there's an actor implementation, the runtime needs to be configured and initialized. To configure the runtime, create a config.php file:

<?php
use Example\Actor\CountActor;
return [
'dapr.actors' => [CountActor::class]
];
view raw config.php hosted with ❤ by GitHub

You can find all the supported keys in the docs too.

There's a relatively simple and opinionated runtime provided out-of-the-box that uses PHP-DI. However, you can bring your own DI container, router, or an entire framework.

Time to initialize the runtime:

<?php
use Dapr\App;
use DI\ContainerBuilder;
require_once __DIR__.'/../vendor/autoload.php';
error_log($_SERVER['REQUEST_URI'], E_USER_WARNING);
$app = App::create(configure: fn(ContainerBuilder $builder) => $builder->addDefinitions(__DIR__.'/config.php'));
$app->start();
view raw index.php hosted with ❤ by GitHub

Here you can see a simple error_log() that outputs the current URI and then the App::create() call where we configure the DI container.

Finally, $app->start() is called, and the built-in routes are handled.

Making a client

Of course, this is no fun if you can't remotely call the service. So, using psysh, a repl can be provided:

<?php
use Dapr\App;
use DI\ContainerBuilder;
require_once __DIR__.'/vendor/autoload.php';
$app = App::create(configure: fn(ContainerBuilder $builder) => $builder->addDefinitions(__DIR__.'/src/config.php'));
view raw repl.php hosted with ❤ by GitHub

Running it

The server

The client

Errata

The code in this post lives here:

Simple Dapr Actor Example

A simple actor that keeps track of a count.

Running the server

$ PHP_CLI_SERVER_WORKERS=10 dapr run --app-id example --app-port 3000 --dapr-http-port 3500 -- php -S 0.0.0.0:3000 src/index.php
Enter fullscreen mode Exit fullscreen mode

Running the repl

$ composer repl
>>>  $counter = $app->run(fn(Dapr\Actors\ActorProxy $proxy) => $proxy->get(Example\Interfaces\CountInterface::class, 'an id'))
=> Dapr\Proxies\dapr_proxy_Count {#2770
     +id: "an id",
     +DAPR_TYPE: "Count",
   }
>>> $counter->getCount()
=> 0
>>> $counter->incrementAndGet(13)
=> 13
>>> $counter->incrementAndGet(13)
=> 26
>>> $counter->incrementAndGet(13)
=> 39
>>> $counter->getCount();
=> 39



You can even fork and fiddle with the unit tests on the php sandbox.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more