In a directory where you want to create your service, run composer init and answer the questions.
Install with composer require dapr/php-sdk and any other dependencies you may wish to use.
Configure your service
Create a config.php, copying the contents below:
<?phpuseDapr\Actors\Generators\ProxyFactory;useDapr\Middleware\Defaults\{Response\ApplicationJson,Tracing};usePsr\Log\LogLevel;usefunctionDI\{env,get};return[// set the log level
'dapr.log.level'=>LogLevel::WARNING,// Generate a new proxy on each request - recommended for development
'dapr.actors.proxy.generation'=>ProxyFactory::GENERATED,// put any subscriptions here
'dapr.subscriptions'=>[],// if this service will be hosting any actors, add them here
'dapr.actors'=>[],// if this service will be hosting any actors, configure how long until dapr should consider an actor idle
'dapr.actors.idle_timeout'=>null,// if this service will be hosting any actors, configure how often dapr will check for idle actors
'dapr.actors.scan_interval'=>null,// if this service will be hosting any actors, configure how long dapr will wait for an actor to finish during drains
'dapr.actors.drain_timeout'=>null,// if this service will be hosting any actors, configure if dapr should wait for an actor to finish
'dapr.actors.drain_enabled'=>null,// you shouldn't have to change this, but the setting is here if you need to
'dapr.port'=>env('DAPR_HTTP_PORT','3500'),// add any custom serialization routines here
'dapr.serializers.custom'=>[],// add any custom deserialization routines here
'dapr.deserializers.custom'=>[],// the following has no effect, as it is the default middlewares and processed in order specified
'dapr.http.middleware.request'=>[get(Tracing::class)],'dapr.http.middleware.response'=>[get(ApplicationJson::class),get(Tracing::class)],];
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:
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.
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.
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:
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.
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
<?phpclassProxyCompiler{privateconstPROXIES=[MyActorInterface::class,MyOtherActorInterface::class,];privateconstPROXY_LOCATION=__DIR__.'/proxies/';publicstaticfunctioncompile(){try{$app=\Dapr\App::create();foreach(self::PROXIESas$interface){$output=$app->run(function(\DI\FactoryInterface$factory)use($interface){return\Dapr\Actors\Generators\FileGenerator::generate($interface,$factory);});$reflection=newReflectionClass($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:
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:
<?phpclassVersionedStateextends\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.
*/publicint$state_version=self::VERSION;/**
* @var int The current version of the data
*/privateconstVERSION=3;/**
* Call when your actor activates.
*/publicfunctionupgrade(){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.
privatefunctionget_previous_version(int$version):int{return$this->has_previous_version($version)?$version-1:$version;}privatefunctionhas_previous_version(int$version):bool{return$version>=0;}privatefunctionwalk_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);}privatefunctionget_versioned_key(string$key,int$version){return$this->has_previous_version($version)?$version.$key:$key;}publicfunction__get(string$key):mixed{return$this->walk_versions(self::VERSION,fn($version)=>parent::__get($this->get_versioned_key($key,$version)),fn($value)=>isset($value));}publicfunction__isset(string$key):bool{return$this->walk_versions(self::VERSION,fn($version)=>parent::__isset($this->get_versioned_key($key,$version)),fn($isset)=>$isset);}publicfunction__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);}publicfunction__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.
2 - The App
Using the App Class
In PHP, there is no default router. Thus, the \Dapr\App class is provided. It uses
Nikic’s FastRoute under the hood. However, you are free to use any router or
framework that you’d like. Just check out the add_dapr_routes() method in the App class to see how actors and
subscriptions are implemented.
Every app should start with App::create() which takes two parameters, the first is an existing DI container, if you
have one, and the second is a callback to hook into the ContainerBuilder and add your own configuration.
From there, you should define your routes and then call $app->start() to execute the route on the current request.
<?php// app.php
require_once__DIR__.'/vendor/autoload.php';$app=\Dapr\App::create(configure:fn(\DI\ContainerBuilder$builder)=>$builder->addDefinitions('config.php'));// add a controller for GET /test/{id} that returns the id
$app->get('/test/{id}',fn(string$id)=>$id);$app->start();
Returning from a controller
You can return anything from a controller, and it will be serialized into a json object. You can also request the
Psr Response object and return that instead, allowing you to customize headers, and have control over the entire response:
<?php$app=\Dapr\App::create(configure:fn(\DI\ContainerBuilder$builder)=>$builder->addDefinitions('config.php'));// add a controller for GET /test/{id} that returns the id
$app->get('/test/{id}',fn(string$id,\Psr\Http\Message\ResponseInterface$response,\Nyholm\Psr7\Factory\Psr17Factory$factory)=>$response->withBody($factory->createStream($id)));$app->start();
Using the app as a client
When you just want to use Dapr as a client, such as in existing code, you can call $app->run(). In these cases, there’s
usually no need for a custom configuration, however, you may want to use a compiled DI container, especially in production:
A DaprClient object is provided, in fact, all the sugar used by the App object is built on the DaprClient.
<?phprequire_once__DIR__.'/vendor/autoload.php';$clientBuilder=\Dapr\Client\DaprClient::clientBuilder();// you can customize (de)serialization or comment out to use the default JSON serializers.
$clientBuilder=$clientBuilder->withSerializationConfig($yourSerializer)->withDeserializationConfig($yourDeserializer);// you can also pass it a logger
$clientBuilder=$clientBuilder->withLogger($myLogger);// and change the url of the sidecar, for example, using https
$clientBuilder=$clientBuilder->useHttpClient('https://localhost:3800')
There are several functions you can call before
2.1 - Unit Testing
Unit Testing
Unit and integration tests are first-class citizens with the PHP SDK. Using the DI container, mocks, stubs,
and the provided \Dapr\Mocks\TestClient allows you to have very fine-grained tests.
Testing Actors
With actors, there are two things we’re interested in while the actor is under test:
The returned result based on an initial state
The resulting state based on the initial state
Here’s an example test a very simple actor that updates its state and returns a specific value:
<?php// TestState.php
classTestStateextends\Dapr\Actors\ActorState{publicint$number;}// TestActor.php
#[\Dapr\Actors\Attributes\DaprType('TestActor')]
classTestActorextends\Dapr\Actors\Actor{publicfunction__construct(string$id,privateTestState$state){parent::__construct($id);}publicfunctionoddIncrement():bool{if($this->state->number%2===0){returnfalse;}$this->state->number+=1;returntrue;}}// TheTest.php
classTheTestextends\PHPUnit\Framework\TestCase{private\DI\Container$container;publicfunctionsetUp():void{parent::setUp();// create a default app and extract the DI container from it
$app=\Dapr\App::create(configure:fn(\DI\ContainerBuilder$builder)=>$builder->addDefinitions(['dapr.actors'=>[TestActor::class]],[\Dapr\DaprClient::class=>\DI\autowire(\Dapr\Mocks\TestClient::class)]));$app->run(fn(\DI\Container$container)=>$this->container=$container);}publicfunctiontestIncrementsWhenOdd(){$id=uniqid();$runtime=$this->container->get(\Dapr\Actors\ActorRuntime::class);$client=$this->getClient();// return the current state from http://localhost:1313/reference/api/actors_api/
$client->register_get("/actors/TestActor/$id/state/number",code:200,data:3);// ensure it increments from http://localhost:1313/reference/api/actors_api/
$client->register_post("/actors/TestActor/$id/state",code:204,response_data:null,expected_request:[['operation'=>'upsert','request'=>['key'=>'number','value'=>4,],],]);$result=$runtime->resolve_actor('TestActor',$id,fn($actor)=>$runtime->do_method($actor,'oddIncrement',null));$this->assertTrue($result);}privatefunctiongetClient():\Dapr\Mocks\TestClient{return$this->container->get(\Dapr\DaprClient::class);}}
When building on transactions, you’ll likely want to test how a failed transaction is handled. In order to do that, you
need to inject failures and ensure the transaction matches what you expect.
<?php// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
classMyStateextends\Dapr\State\TransactionalState{publicstring$value='';}// SomeService.php
classSomeService{publicfunction__construct(privateMyState$state){}publicfunctiondoWork(){$this->state->begin();$this->state->value="hello world";$this->state->commit();}}// TheTest.php
classTheTestextends\PHPUnit\Framework\TestCase{private\DI\Container$container;publicfunctionsetUp():void{parent::setUp();$app=\Dapr\App::create(configure:fn(\DI\ContainerBuilder$builder)=>$builder->addDefinitions([\Dapr\DaprClient::class=>\DI\autowire(\Dapr\Mocks\TestClient::class)]));$this->container=$app->run(fn(\DI\Container$container)=>$container);}privatefunctiongetClient():\Dapr\Mocks\TestClient{return$this->container->get(\Dapr\DaprClient::class);}publicfunctiontestTransactionFailure(){$client=$this->getClient();// create a response from https://v1-16.docs.dapr.io/reference/api/state_api/
$client->register_post('/state/statestore/bulk',code:200,response_data:[['key'=>'value',// no previous value
],],expected_request:['keys'=>['value'],'parallelism'=>10]);$client->register_post('/state/statestore/transaction',code:200,response_data:null,expected_request:['operations'=>[['operation'=>'upsert','request'=>['key'=>'value','value'=>'hello world']]]]);$state=newMyState($this->container,$this->container);$service=newSomeService($state);$service->doWork();$this->assertSame('hello world',$state->value);}}
Dapr uses JSON serialization and thus (complex) type information is lost when sending/receiving data.
Serialization
When returning an object from a controller, passing an object to the DaprClient, or storing an object in a state store,
only public properties are scanned and serialized. You can customize this behavior by implementing \Dapr\Serialization\ISerialize.
For example, if you wanted to create an ID type that serialized to a string, you may implement it like so:
This works for any type that we have full ownership over, however, it doesn’t work for classes from libraries or PHP itself.
For that, you need to register a custom serializer with the DI container:
<?php// in config.php
classSerializeSomeClassimplements\Dapr\Serialization\Serializers\ISerialize{publicfunctionserialize(mixed$value,\Dapr\Serialization\ISerializer$serializer):mixed{// serialize $value and return the result
}}return['dapr.serializers.custom'=>[SomeClass::class=>newSerializeSomeClass()],];
Deserialization
Deserialization works exactly the same way, except the interface is \Dapr\Deserialization\Deserializers\IDeserialize.
4 - Publish and Subscribe with PHP
How to use
With Dapr, you can publish anything, including cloud events. The SDK contains a simple cloud event implementation, but
you can also just pass an array that conforms to the cloud event spec or use another library.
Only <code>application/octet-steam</code> is supported for binary data.
Receiving cloud events
In your subscription handler, you can have the DI Container inject either a Dapr\PubSub\CloudEvent or an array into
your controller. The former does some validation to ensure you have a proper event. If you need direct access to the
data, or the events do not conform to the spec, use an array.
5 - State Management with PHP
How to use
Dapr offers a great modular approach to using state in your application. The best way to learn the basics is to visit
the howto.
Metadata
Many state components allow you to pass metadata to the component to control specific aspects of the component’s
behavior. The PHP SDK allows you to pass that metadata through:
<?php// using the state manager
$app->run(fn(\Dapr\State\StateManager$stateManager)=>$stateManager->save_state('statestore',new\Dapr\State\StateItem('key','value',metadata:['port'=>'112'])));// using the DaprClient
$app->run(fn(\Dapr\Client\DaprClient$daprClient)=>$daprClient->saveState(storeName:'statestore',key:'key',value:'value',metadata:['port'=>'112']))
This is an example of how you might pass the port metadata to Cassandra.
Every state operation allows passing metadata.
Consistency/concurrency
In the PHP SDK, there are four classes that represent the four different types of consistency and concurrency in Dapr:
Passing one of them to a StateManager method or using the StateStore() attribute allows you to define how the state
store should handle conflicts.
Parallelism
When doing a bulk read or beginning a transaction, you can specify the amount of parallelism. Dapr will read “at most”
that many keys at a time from the underlying store if it has to read one key at a time. This can be helpful to control
the load on the state store at the expense of performance. The default is 10.
Prefix
Hardcoded key names are useful, but why not make state objects more reusable? When committing a transaction or saving an
object to state, you can pass a prefix that is applied to every key in the object.
<?phpclassTransactionObjectextends\Dapr\State\TransactionalState{publicstring$key;}$app->run(function(TransactionObject$object){$object->begin(prefix:'my-prefix-');$object->key='value';// commit to key `my-prefix-key`
$object->commit();});
<?phpclassStateObject{publicstring$key;}$app->run(function(\Dapr\State\StateManager$stateManager){$stateManager->load_object($obj=newStateObject(),prefix:'my-prefix-');// original value is from `my-prefix-key`
$obj->key='value';// save to `my-prefix-key`
$stateManager->save_object($obj,prefix:'my-prefix-');});