Dapr PHP SDK 用于开发 Dapr 应用程序的 PHP SDK 包
Dapr 提供了一个 SDK 来帮助开发 PHP 应用程序。使用它,你可以用 Dapr 创建 PHP 客户端、服务器和虚拟 Actor。
设置 前提条件 可选前提条件 初始化项目 在你想要创建服务的目录中,运行 composer init 并回答问题。
使用 composer require dapr/php-sdk 以及你可能希望使用的任何其他依赖项进行安装。
配置服务 创建一个 config.php,复制以下内容:
<? php
use Dapr\Actors\Generators\ProxyFactory ;
use Dapr\Middleware\Defaults\ { Response\ApplicationJson , Tracing };
use Psr\Log\LogLevel ;
use function DI\ { env , get };
return [
// 设置日志级别
'dapr.log.level' => LogLevel :: WARNING ,
// 在每个请求上生成一个新的代理 - 建议在开发中使用
'dapr.actors.proxy.generation' => ProxyFactory :: GENERATED ,
// 在此处添加任何订阅
'dapr.subscriptions' => [],
// 如果此服务将托管任何 Actor,请在此添加它们
'dapr.actors' => [],
// 如果此服务将托管任何 Actor,配置 Dapr 应将 Actor 视为空闲的时间
'dapr.actors.idle_timeout' => null ,
// 如果此服务将托管任何 Actor,配置 Dapr 检查空闲 Actor 的频率
'dapr.actors.scan_interval' => null ,
// 如果此服务将托管任何 Actor,配置 Dapr 在排空期间等待 Actor 完成的时间
'dapr.actors.drain_timeout' => null ,
// 如果此服务将托管任何 Actor,配置 Dapr 是否应等待 Actor 完成
'dapr.actors.drain_enabled' => null ,
// 你通常不需要更改此项,但如果需要可以在此设置
'dapr.port' => env ( 'DAPR_HTTP_PORT' , '3500' ),
// 在此处添加任何自定义序列化例程
'dapr.serializers.custom' => [],
// 在此处添加任何自定义反序列化例程
'dapr.deserializers.custom' => [],
// 以下内容无效,因为它是默认中间件并按指定顺序处理
'dapr.http.middleware.request' => [ get ( Tracing :: class )],
'dapr.http.middleware.response' => [ get ( ApplicationJson :: class ), get ( Tracing :: class )],
];
创建服务 创建 index.php 并放入以下内容:
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
use Dapr\App ;
$app = App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( __DIR__ . '/config.php' ));
$app -> get ( '/hello/{name}' , function ( string $name ) {
return [ 'hello' => $name ];
});
$app -> start ();
试用 使用 dapr init 初始化 dapr,然后使用 dapr run -a dev -p 3000 -- php -S 0.0.0.0:3000 启动项目。
现在你可以打开 Web 浏览器并访问 http://localhost:3000/hello/world
将 world 替换为你的名字、宠物的名字或任何你想要的内容。
恭喜,你已经创建了你的第一个 Dapr 服务!我很期待看到你用它做什么!
更多信息 1 - 虚拟 Actor 如何构建 Actor
如果你不熟悉 Actor 模式,了解 Actor 模式的最佳位置是
Actor 概述。
在 PHP SDK 中,Actor 有两个方面,即客户端和 Actor(也称为运行时)。作为 Actor 的客户端,
你将通过 ActorProxy 类与远程 Actor 交互。该类使用若干配置策略之一动态地生成代理类。
在编写 Actor 时,状态可以为你管理。你可以钩入 Actor 生命周期,并定义提醒和定时器。
这为你处理适合 Actor 模式的各类问题提供了相当大的能力。
Actor 代理 每当您需要与 Actor 通信时,都需要获取一个代理对象来执行此操作。代理负责
序列化您的请求、反序列化响应并将其返回给您,同时遵守指定接口定义的契约。
为了创建代理,首先需要一个接口来定义您与 Actor 发送和接收的内容和方式。
例如,如果您想与一个仅跟踪计数的计数 Actor 通信,您可以将接口
定义如下:
<? php
#[\Dapr\Actors\Attributes\DaprType('Counter')]
interface ICount {
function increment ( int $amount = 1 ) : void ;
function get_count () : int ;
}
将此接口放在 Actor 和客户端都可以访问的共享库中(如果两者都用 PHP 编写),这是一个好主意。DaprType
属性告诉 DaprClient 要发送到的 Actor 名称。它应该与实现的 DaprType 匹配,尽管
如果需要,您可以覆盖该类型。
<? php
$app -> run ( function ( \Dapr\Actors\ActorProxy $actorProxy ) {
$actor = $actorProxy -> get ( ICount :: class , 'actor-id' );
$actor -> increment ( 10 );
});
编写 Actor 要创建 Actor,您需要实现之前定义的接口,并添加 DaprType 属性。所有
Actor 必须 实现 IActor,不过有一个 Actor 基类实现了样板代码,使您的实现
更加简单。
以下是计数 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 ;
}
}
最重要的是构造函数。它至少接受一个名为 id 的参数,该参数是 Actor 的 id。
任何其他参数都由 DI 容器注入,包括您想要使用的任何 ActorState。
Actor 生命周期 Actor 通过构造函数在每个针对该 Actor 类型的请求上进行实例化。您可以使用它来计算
临时状态或处理您所需的任何特定于请求的启动,例如设置其他客户端或
连接。
实例化 Actor 后,可能会调用 on_activation() 方法。on_activation() 方法在 Actor “唤醒”
或首次创建时调用。它不会在每次请求时调用。
接下来,调用 Actor 方法。这可能来自定时器、提醒或客户端。您可以执行任何需要
完成的工作和/或抛出异常。
最后,工作结果返回给调用者。一段时间后(取决于您如何配置
服务),Actor 将被停用,并将调用 on_deactivation()。如果主机宕机、
daprd 崩溃或发生其他一些阻止其成功调用的错误,则可能不会调用此方法。
Actor 状态 Actor 状态是扩展 ActorState 的"普通旧 PHP 对象"(POPO)。ActorState 基类提供了一些
有用的方法。以下是示例实现:
<? php
class CountState extends \Dapr\Actors\ActorState {
public int $count = 0 ;
}
注册 Actor Dapr 期望在启动时知道服务可以托管哪些 Actor。您需要将其添加到配置中:
如果你想利用预编译的依赖注入,你需要使用一个工厂:
<? php
// in config.php
return [
'dapr.actors' => fn () => [ Counter :: class ],
];
启动应用程序所需的全部内容:
<? 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 ]
];
启动应用程序所需的全部内容:
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ));
$app -> start ();
1.1 - 生产环境参考:Actor 在生产环境中运行 PHP Actor
代理模式 Actor 代理有四种不同的处理模式。每种模式呈现不同的权衡,你需要在开发和生产环境中进行权衡。
<? php
\Dapr\Actors\Generators\ProxyFactory :: GENERATED ;
\Dapr\Actors\Generators\ProxyFactory :: GENERATED_CACHED ;
\Dapr\Actors\Generators\ProxyFactory :: ONLY_EXISTING ;
\Dapr\Actors\Generators\ProxyFactory :: DYNAMIC ;
可以通过 dapr.actors.proxy.generation 配置键进行设置。
GENERATED
GENERATED_CACHED
ONLY_EXISTING
DYNAMIC 这是默认模式。在此模式下,每个请求都会生成一个类并通过 eval 执行。它主要用于开发,不应在生产环境中使用。
这与 ProxyModes::GENERATED 相同,只是类存储在临时文件中,因此不需要在每次请求时重新生成。它不知道何时更新缓存的类,因此不建议在开发中使用,但在无法手动生成文件时提供此选项。
在此模式下,如果代理类不存在,将抛出异常。当你不想在生产环境中生成代码时,这很有用。你需要确保生成并预加载/自动加载该类。
生成代理 你可以创建一个 composer 脚本按需生成代理,以利用 ONLY_EXISTING 模式。
创建一个 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 " ;
}
}
}
然后为生成的代理添加一个 psr-4 自动加载器,并在 composer.json 中添加一个脚本:
{
"autoload" : {
"psr-4" : {
"Dapr\\Proxies\\" : "path/to/proxies"
}
},
"scripts" : {
"compile-proxies" : "ProxyCompiler::compile"
}
}
最后,配置 dapr 仅使用生成的代理:
<? php
// in config.php
return [
'dapr.actors.proxy.generation' => ProxyFactory :: ONLY_EXISTING ,
];
在此模式下,代理满足接口约定,但实际上并不实现接口本身(意味着 instanceof 将返回 false)。此模式利用 PHP 的一些特性来工作,适用于代码无法被 eval 或生成的场景。
请求 对于任何模式,创建 actor 代理的开销都非常小。创建 actor 代理对象时不会发起任何请求。
当你在代理对象上调用方法时,只有你实现的方法由你的 actor 实现提供服务。get_id() 在本地处理,而 get_reminder()、delete_reminder() 等由 daprd 处理。
Actor 实现 PHP 中的每个 actor 实现都必须实现 \Dapr\Actors\IActor 并使用 \Dapr\Actors\ActorTrait trait。这允许快速反射并提供一些快捷方式。使用 \Dapr\Actors\Actor 抽象基类会自动为你完成这些,但如果你需要覆盖默认行为,可以通过实现接口并使用该 trait 来实现。
激活和停用 当 actor 激活时,会将一个令牌文件写入临时目录(在 Linux 上默认为 '/tmp/dapr_' + sha256(concat(Dapr type, id)),在 Windows 上为 '%temp%/dapr_' + sha256(concat(Dapr type, id)))。该文件会一直保留,直到 actor 停用或主机关闭。这确保了当 Dapr 在主机上激活 actor 时,on_activation 只被调用一次。
性能 在使用 php-fpm 和 nginx 或 Windows 上的 IIS 的生产环境中,actor 方法调用非常快。虽然 actor 在每个请求上都会被构造,但 actor 状态键仅在需要时加载,而不是在每个请求期间加载。但是,单独加载每个键会有一些开销。可以通过在状态中存储数据数组来缓解这个问题,以可用性换取速度。不建议从一开始就这样做,而是作为需要时的优化手段。
状态版本控制 ActorState 对象中的变量名直接对应存储中的键名。这意味着如果你更改变量的类型或名称,可能会遇到错误。为了解决这个问题,你可能需要对状态对象进行版本控制。为此,你需要覆盖状态的加载和存储方式。有很多方法可以解决这个问题,其中一种解决方案可能如下所示:
<? php
class VersionedState extends \Dapr\Actors\ActorState {
/**
* @var int 存储中状态的当前版本。我们给出当前版本的默认值。
* 但是,它在存储中可能有不同的值。
*/
public int $state_version = self :: VERSION ;
/**
* @var int 数据的当前版本
*/
private const VERSION = 3 ;
/**
* 在你的 actor 激活时调用。
*/
public function upgrade () {
if ( $this -> state_version < self :: VERSION ) {
$value = parent :: __get ( $this -> get_versioned_key ( 'key' , $this -> state_version ));
// 更新数据结构后更新值
parent :: __set ( $this -> get_versioned_key ( 'key' , self :: VERSION ), $value );
$this -> state_version = self :: VERSION ;
$this -> save_state ();
}
}
// 如果你在上面的方法中根据需要升级所有键,则不需要在加载/保存时遍历以前的
// 键,你只需获取键的当前版本。
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 {
// 可选:你可以取消设置键的以前版本
parent :: __set ( $this -> get_versioned_key ( $key , self :: VERSION ), $value );
}
public function __unset ( string $key ) : void {
// 取消设置此版本和所有以前版本
$this -> walk_versions (
self :: VERSION ,
fn ( $version ) => parent :: __unset ( $this -> get_versioned_key ( $key , $version )),
fn () => false
);
}
}
有很多可以优化的地方,直接在生产环境中使用它并不是一个好主意,但你可以了解它的工作原理。其中大部分将取决于你的用例,这也是 SDK 中没有类似功能的原因。例如,在此示例实现中,保留以前的值是为了防止升级期间可能出现的错误;保留以前的值允许再次运行升级,但你可能希望删除以前的值。
2 - 使用 PHP 进行发布订阅 如何使用
使用 Dapr,你可以发布任何内容,包括云事件。SDK 包含一个简单的云事件实现,但你也可以直接传递一个符合云事件规范的数组,或者使用其他库。
<? php
$app -> post ( '/publish' , function ( \Dapr\Client\DaprClient $daprClient ) {
$daprClient -> publishEvent ( pubsubName : 'pubsub' , topicName : 'my-topic' , data : [ 'something' => 'happened' ]);
});
有关发布/订阅的更多信息,请查看操作指南 。
数据内容类型 PHP SDK 允许在构造自定义云事件或发布原始数据时设置数据内容类型。
<? php
$event = new \Dapr\PubSub\CloudEvent ();
$event -> data = $xml ;
$event -> data_content_type = 'application/xml' ;
<? php
/**
* @var \Dapr\Client\DaprClient $daprClient
*/
$daprClient -> publishEvent ( pubsubName : 'pubsub' , topicName : 'my-topic' , data : $raw_data , contentType : 'application/octet-stream' );
Binary data 二进制数据仅支持 <code>application/octet-steam</code>。
接收云事件 在你的订阅处理程序中,你可以让 DI 容器向你的控制器中注入 Dapr\PubSub\CloudEvent 或 array。前者会进行一些验证以确保你有一个正确的事件。如果你需要直接访问数据,或者事件不符合规范,请使用 array。
3 - 应用 使用 App 类
在 PHP 中,没有默认的路由器。因此,提供了 \Dapr\App 类。它在底层使用
Nikic’s FastRoute 。不过,您可以根据需要自由使用任何路由器或
框架。只需查看 App 类中的 add_dapr_routes() 方法,即可了解 actors 和
订阅是如何实现的。
每个应用都应从 App::create() 开始,它接受两个参数,第一个是现有的 DI 容器(如果
您有的话),第二个是回调函数,用于钩入 ContainerBuilder 并添加您自己的配置。
在此基础上,您应该定义路由,然后调用 $app->start() 来执行当前请求的路由。
<? php
// app.php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ));
// 添加一个 GET /test/{id} 的控制器,返回 id
$app -> get ( '/test/{id}' , fn ( string $id ) => $id );
$app -> start ();
从控制器返回 您可以从控制器返回任何内容,它将被序列化为 json 对象。您也可以请求
Psr Response 对象并返回该对象,从而允许您自定义标头,并对整个响应进行控制:
<? php
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ));
// 添加一个 GET /test/{id} 的控制器,返回 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 ();
将应用作为客户端使用 当您只想将 Dapr 用作客户端时,例如在现有代码中,可以调用 $app->run()。在这些情况下,通常
不需要自定义配置,不过,您可能希望使用编译后的 DI 容器,特别是在生产环境中:
<? php
// app.php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> enableCompilation ( __DIR__ ));
$result = $app -> run ( fn ( \Dapr\DaprClient $client ) => $client -> get ( '/invoke/other-app/method/my-method' ));
在其他框架中使用 提供了 DaprClient 对象,实际上,App 对象使用的所有便捷方法都是基于 DaprClient 构建的。
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
$clientBuilder = \Dapr\Client\DaprClient :: clientBuilder ();
// 您可以自定义(反)序列化,或注释掉以使用默认的 JSON 序列化器。
$clientBuilder = $clientBuilder -> withSerializationConfig ( $yourSerializer ) -> withDeserializationConfig ( $yourDeserializer );
// 您还可以传入一个 logger
$clientBuilder = $clientBuilder -> withLogger ( $myLogger );
// 以及更改边车的 url,例如,使用 https
$clientBuilder = $clientBuilder -> useHttpClient ( 'https://localhost:3800' )
在调用之前,您可以调用多个函数
3.1 - 单元测试 单元测试
单元测试和集成测试是 PHP SDK 的一等公民。通过使用 DI 容器、mock、stub 以及提供的 \Dapr\Mocks\TestClient,你可以编写非常细粒度的测试。
测试 Actor 在测试 Actor 时,我们关注两件事:
基于初始状态的返回结果 基于初始状态的最终状态
使用 TestClient 进行集成测试
单元测试 下面是一个测试非常简单的 actor 的示例,该 actor 更新其状态并返回特定值:
<? php
// TestState.php
class TestState extends \Dapr\Actors\ActorState
{
public int $number ;
}
// TestActor.php
#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
public function __construct ( string $id , private TestState $state )
{
parent :: __construct ( $id );
}
public function oddIncrement () : bool
{
if ( $this -> state -> number % 2 === 0 ) {
return false ;
}
$this -> state -> number += 1 ;
return true ;
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase
{
private \DI\Container $container ;
public function setUp () : void
{
parent :: setUp ();
// 创建一个默认应用并从中提取 DI 容器
$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 );
}
public function testIncrementsWhenOdd ()
{
$id = uniqid ();
$runtime = $this -> container -> get ( \Dapr\Actors\ActorRuntime :: class );
$client = $this -> getClient ();
// 从 http://localhost:1313/reference/api/actors_api/ 返回当前状态
$client -> register_get ( "/actors/TestActor/ $id /state/number" , code : 200 , data : 3 );
// 确保从 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 );
}
private function getClient () : \Dapr\Mocks\TestClient
{
return $this -> container -> get ( \Dapr\DaprClient :: class );
}
}
<? php
// TestState.php
class TestState extends \Dapr\Actors\ActorState
{
public int $number ;
}
// TestActor.php
#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
public function __construct ( string $id , private TestState $state )
{
parent :: __construct ( $id );
}
public function oddIncrement () : bool
{
if ( $this -> state -> number % 2 === 0 ) {
return false ;
}
$this -> state -> number += 1 ;
return true ;
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase
{
public function testNotIncrementsWhenEven () {
$container = new \DI\Container ();
$state = new TestState ( $container , $container );
$state -> number = 4 ;
$id = uniqid ();
$actor = new TestActor ( $id , $state );
$this -> assertFalse ( $actor -> oddIncrement ());
$this -> assertSame ( 4 , $state -> number );
}
}
测试事务 在基于事务构建时,你可能需要测试如何处理失败的事务。为此,你需要注入故障并确保事务符合你的预期。
使用 TestClient 进行集成测试
单元测试 <? php
// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
public string $value = '' ;
}
// SomeService.php
class SomeService {
public function __construct ( private MyState $state ) {}
public function doWork () {
$this -> state -> begin ();
$this -> state -> value = "hello world" ;
$this -> state -> commit ();
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
private \DI\Container $container ;
public function setUp () : 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 );
}
private function getClient () : \Dapr\Mocks\TestClient {
return $this -> container -> get ( \Dapr\DaprClient :: class );
}
public function testTransactionFailure () {
$client = $this -> getClient ();
// 从 https://docs.dapr.io/zh-hans/reference/api/state_api/ 创建响应
$client -> register_post ( '/state/statestore/bulk' , code : 200 , response_data : [
[
'key' => '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 = new MyState ( $this -> container , $this -> container );
$service = new SomeService ( $state );
$service -> doWork ();
$this -> assertSame ( 'hello world' , $state -> value );
}
}
<? php
// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
public string $value = '' ;
}
// SomeService.php
class SomeService {
public function __construct ( private MyState $state ) {}
public function doWork () {
$this -> state -> begin ();
$this -> state -> value = "hello world" ;
$this -> state -> commit ();
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
public function testTransactionFailure () {
$state = $this -> createStub ( MyState :: class );
$service = new SomeService ( $state );
$service -> doWork ();
$this -> assertSame ( 'hello world' , $state -> value );
}
}
4 - 使用 PHP 进行状态管理 如何使用
Dapr 为在应用程序中使用状态提供了一种优秀的模块化方法。学习基础知识的最佳方式是访问
操作指南 。
元数据 许多状态组件允许您向组件传递元数据以控制组件行为的特定方面。PHP SDK 允许您通过以下方式传递该元数据:
<? php
// 使用 state manager
$app -> run (
fn ( \Dapr\State\StateManager $stateManager ) =>
$stateManager -> save_state ( 'statestore' , new \Dapr\State\StateItem ( 'key' , 'value' , metadata : [ 'port' => '112' ])));
// 使用 DaprClient
$app -> run ( fn ( \Dapr\Client\DaprClient $daprClient ) => $daprClient -> saveState ( storeName : 'statestore' , key : 'key' , value : 'value' , metadata : [ 'port' => '112' ]))
这是向 Cassandra 传递端口元数据的示例。
每个状态操作都允许传递元数据。
一致性/并发 在 PHP SDK 中,有四个类代表 Dapr 中四种不同类型的一致性和并发:
<? php
[
\Dapr\consistency\StrongLastWrite :: class ,
\Dapr\consistency\StrongFirstWrite :: class ,
\Dapr\consistency\EventualLastWrite :: class ,
\Dapr\consistency\EventualFirstWrite :: class ,
]
将其中之一传递给 StateManager 方法或使用 StateStore() 属性,您可以定义状态存储应如何处理冲突。
并行度 执行批量读取或开始事务时,您可以指定并行度数量。如果 Dapr 必须一次读取一个键,它将从底层存储中"最多"一次读取该数量的键。这有助于控制状态存储上的负载,但会降低性能。默认值为 10。
前缀 硬编码的键名称很有用,但为什么不使状态对象更具可重用性呢?在提交事务或将对象保存到状态时,您可以传递一个应用于对象中每个键的前缀。
Transaction prefix
StateManager prefix <? php
class TransactionObject extends \Dapr\State\TransactionalState {
public string $key ;
}
$app -> run ( function ( TransactionObject $object ) {
$object -> begin ( prefix : 'my-prefix-' );
$object -> key = 'value' ;
// 提交到键 `my-prefix-key`
$object -> commit ();
});
<? php
class StateObject {
public string $key ;
}
$app -> run ( function ( \Dapr\State\StateManager $stateManager ) {
$stateManager -> load_object ( $obj = new StateObject (), prefix : 'my-prefix-' );
// 原始值来自 `my-prefix-key`
$obj -> key = 'value' ;
// 保存到 `my-prefix-key`
$stateManager -> save_object ( $obj , prefix : 'my-prefix-' );
});
5 - 自定义序列化 如何配置序列化
Dapr 使用 JSON 序列化,因此在发送/接收数据时会丢失(复杂)类型信息。
序列化 当从控制器返回对象、将对象传递给 DaprClient,或将对象存储在状态存储中时,
只有公共属性会被扫描和序列化。你可以通过实现 \Dapr\Serialization\ISerialize 来自定义此行为。
例如,如果你想创建一个序列化为字符串的 ID 类型,可以像这样实现:
<? php
class MyId implements \Dapr\Serialization\Serializers\ISerialize
{
public string $id ;
public function serialize ( mixed $value , \Dapr\Serialization\ISerializer $serializer ) : mixed
{
// $value === $this
return $this -> id ;
}
}
这适用于我们拥有完全所有权的任何类型,但它不适用于来自库或 PHP 本身的类。
为此,你需要向 DI 容器注册自定义序列化器:
<? php
// 在 config.php 中
class SerializeSomeClass implements \Dapr\Serialization\Serializers\ISerialize
{
public function serialize ( mixed $value , \Dapr\Serialization\ISerializer $serializer ) : mixed
{
// 序列化 $value 并返回结果
}
}
return [
'dapr.serializers.custom' => [ SomeClass :: class => new SerializeSomeClass ()],
];
反序列化 反序列化的工作方式完全相同,只是接口是 \Dapr\Deserialization\Deserializers\IDeserialize。