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

Return to the regular view of this page.

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配置键进行设置。

这是默认模式。在此模式下,每个请求都会生成一个类并通过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-fpmnginx或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');

接收云事件

在您的订阅处理程序中,您可以让 DI 容器将 Dapr\PubSub\CloudEventarray 注入到您的控制器中。使用 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 时,我们主要关注两个方面:

  1. 基于初始状态的返回结果
  2. 基于初始状态的结果状态

以下是一个简单的 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://v1-16.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