Dapr PHP SDK 用于开发Dapr应用的PHP SDK包
Dapr提供了一个SDK,帮助开发PHP应用程序。通过它,您可以使用Dapr创建PHP客户端、服务器和虚拟actor。
设置 先决条件 可选先决条件 初始化您的项目 在您希望创建服务的目录中,运行composer init并回答提示的问题。
使用composer require dapr/php-sdk安装此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'                   =>  [], 
    
     // 配置Dapr在多长时间后认为actor空闲
  'dapr.actors.idle_timeout'      =>  null , 
    
     // 配置Dapr检查空闲actor的频率
  'dapr.actors.scan_interval'     =>  null , 
    
     // 配置Dapr在关闭期间等待actor完成的时间
  'dapr.actors.drain_timeout'     =>  null , 
    
     // 配置Dapr是否应等待actor完成
  'dapr.actors.drain_enabled'     =>  null , 
    
     // 您可以在此处更改Dapr的端口设置
  '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启动项目。
您现在可以打开一个网页浏览器并访问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 State actor状态是一个扩展ActorState的“普通旧PHP对象”(POPO)。ActorState基类提供了一些有用的方法。以下是一个示例实现:
<? php 
class  CountState  extends  \Dapr\Actors\ActorState  { 
    public  int  $count  =  0 ; 
 } 
注册Actor Dapr期望在启动时知道服务可能托管的actor。你需要将其添加到配置中:
如果你想利用预编译的依赖注入,你需要使用工厂:
<? php 
// 在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 
// 在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代理有四种模式可供选择。每种模式都有不同的优缺点,您需要在开发和生产中进行权衡。
<? 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 " ; 
         } 
     } 
 } 
然后在composer.json中为生成的代理添加一个psr-4自动加载器和一个脚本:
{ 
  "autoload" :  { 
     "psr-4" :  { 
       "Dapr\\Proxies\\" :  "path/to/proxies" 
     } 
   }, 
   "scripts" :  { 
     "compile-proxies" :  "ProxyCompiler::compile" 
   } 
 } 
最后,配置dapr仅使用生成的代理:
<? php 
// 在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特性。这允许快速反射和一些快捷方式。使用\Dapr\Actors\Actor抽象基类可以为您做到这一点,但如果您需要覆盖默认行为,可以通过实现接口和使用特性来实现。
激活和停用 当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' ); 
二进制数据 对于二进制数据,仅支持 <code>application/octet-stream</code>。
接收云事件 在您的订阅处理程序中,您可以让 DI 容器将 Dapr\PubSub\CloudEvent 或 array 注入到您的控制器中。使用 Dapr\PubSub\CloudEvent 时,会进行一些验证以确保事件的正确性。如果您需要直接访问数据,或者事件不符合规范,请使用 array。
3 - 应用程序 使用 App 类
PHP 中没有默认的路由器。因此,提供了 \Dapr\App 类。它底层使用了 Nikic 的 FastRoute 。然而,你可以选择任何你喜欢的路由器或框架。只需查看 App 类中的 add_dapr_routes() 方法,了解 actor 和订阅是如何实现的。
每个应用程序都应该以 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 ); 
 // 你也可以传递一个日志记录器
 $clientBuilder  =  $clientBuilder -> withLogger ( $myLogger ); 
 // 并更改 sidecar 的 URL,例如,使用 https
 $clientBuilder  =  $clientBuilder -> useHttpClient ( 'https://localhost:3800' )  
在调用之前有几个函数可以使用
3.1 - 单元测试 单元测试
在 PHP SDK 中,单元测试和集成测试是非常重要的组成部分。通过使用依赖注入容器、模拟、存根以及提供的 \Dapr\Mocks\TestClient,可以实现非常精细的测试。
测试 Actor 在测试 Actor 时,我们主要关注两个方面:
基于初始状态的返回结果 基于初始状态的结果状态 以下是一个简单的 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 ); 
     } 
 } 
测试事务 在构建事务时,您可能需要测试如何处理失败的事务。为此,您需要注入故障并确保事务按预期进行。
<? 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 
// 使用状态管理器
 $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。
前缀 硬编码的键名很有用,但让状态对象更具可重用性会更好。在提交事务或将对象保存到状态时,您可以传递一个前缀,该前缀应用于对象中的每个键。
<? 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 自带的类。对于这些情况,您需要在依赖注入容器中注册一个自定义序列化器:
<? 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。