bottledcode/durable-php

v2.0.1 2024-05-12 14:04 UTC

README

这是一个允许构建全球、分布式和持久性PHP应用的框架/库。它受到了几个微软项目的启发

  1. Orleans
  2. 服务网格
  3. 持久函数
  4. Dapr

以及Akka、Erlang等其他基于actor的框架。

它是什么?

Durable PHP是一个框架/库,允许你构建持久性分布式应用。持久性意味着应用可以承受单个组件的失败,并且应用可以在不中断服务的情况下进行扩展和缩减。Durable PHP使用RethinkDb或Redis作为底层存储。

它是如何工作的?

Durable PHP的构建块被称为“活动”。活动是一个无状态的类,实现了一个方法或可调用的功能。活动可以调用其他活动、进行计算、执行I/O操作和等待事件。活动由工作进程执行。工作进程负责执行活动并在数据库中存储活动的状态。

“编排”建立在活动之上。编排是一个有状态的类或方法,可以调用其他编排和活动。编排的状态存储在数据库中,状态是通过“回放”编排来重建的。因此,不建议在编排中进行I/O操作,因为这将执行多次。

“actor”建立在编排之上。actor是一个有状态的类或方法,可以调用其他actor、执行I/O操作和等待事件。actor的状态存储在数据库中,状态是通过填充actor来重建的。

这与常规PHP有何不同?

常规PHP是无状态的。这意味着每个请求都由一个新的PHP进程处理。这对于性能和可扩展性来说很好,但它使得构建需要维护状态的应用变得困难——通常涉及ORM和加载状态的仪式。Durable PHP允许你构建可以维护状态的应用,并且可以承受单个组件的失败。Durable PHP还设计用于在分布式环境中使用,其中多个PHP进程在不同的机器上运行,不会发生死锁。

这与其他PHP框架有何不同?

Durable PHP鼓励丰富的模型和领域驱动设计(DDD)。这意味着你可以使用代表你领域的类和方法来构建你的应用。

我能看到一个示例吗?

想象一下,你想上传一个文件到S3并等待文件处理完成,然后向用户发送邮件或如果发生错误或耗时过长,则向管理员发送邮件。没有Durable-PHP,这将非常复杂。以下是使用Durable-PHP的示例:

# SendEmailActivity.php

class SendEmailActivity {
    public function __invoke(string $to, string $subject, string $body) {
        // Send email
    }
}

# UploadEntityInterface.php

interface UploadEntityInterface {
    public function getFileUrl(): string;
    public function setProcessState(string $state): void;
    public function getProcessState(): string;
}

# UploadEntity.php

class UploadEntity extends \Bottledcode\DurablePhp\State\EntityState implements UploadEntityInterface {
    public function __construct(private string $url, private string $state = 'pending') {}
    public function setProcessState(string $state): void {
        $this->state = $state;
    }
    public function getProcessState(): string {
        return $this->state;
    }
}

# UploadOrchestration.php

class UploadOrchestration {
    public function __construct(private \Bottledcode\DurablePhp\OrchestrationContextInterface $context) {}
   
    public function __invoke(string $url) {
        $ctx = $this->context();
        $uploadId = $ctx->newUuid();

        // get a future that will be resolved when the upload is processed
        $signal = $context->waitForExternalEvent('upload-processed');

        // get a future that will be resolved when the timer expires (one hour from now)
        $timeout = $context->createTimer($context->getCurrentTime()->add(new DateInterval('PT1H')))

        // wait for the upload to be processed or the timer to expire
        $winner = $context->waitAny($signal, $timeout);

        if($winner === $signal) {
            // upload was processed
            $ctx->signal($uploadId, fn(UploadEntity $entity) => $entity->setProcessState('processed'));
            $context->callActivity(SendEmailActivity::class, ['to' => 'user', 'subject' => 'Upload processed', 'body' => 'Your upload was processed']);
        } else {
            // upload was not processed
            $ctx->signal($uploadId, fn(UploadEntity $entity) => $entity->setProcessState('timed-out'));
            $context->callActivity(SendEmailActivity::class, ['to' => 'admin', 'subject' => 'Upload failed', 'body' => 'The upload timed out']);
        }
    }
}

我该如何使用它?

这远未达到生产就绪状态,所以如果你想使用它,请做好准备,因为可能会出现故障或在没有太多警告的情况下进行大幅更改。如果你在使用,请通过问题反馈给我。我很乐意了解它的使用情况和用途。