event-sourcery/event-sourcery

支持GDPR的极简PHP事件源/CQRS库

7.12 2022-06-03 22:23 UTC

README

事件源/CQRS框架,其核心原则是保持简单。

该库处于概念开发阶段。请勿使用。

该库非常不稳定,会迅速变化。它正在多个生产项目中实现,并将很快稳定下来。

许多实现都是临时性的,其中一些甚至未经测试。到1.0版本发布时,将根据当前形式使用经验重新整理整个库。

目录

核心价值观/概念是

  1. 减少开发者需要编写/测试的移动部件数量。
  2. 使用简单直观的代码实现,可以轻松地通过您最喜欢的IDE进行分析。
  3. 没有自定义事件序列化。将任何序列化推送到值对象,以便您只需对创建的每个事件进行一次序列化和测试。
  4. 可选的个人数据值对象存储到单独的数据存储中。事件保持在典型的事件存储中。可以轻松移除调用“删除权”的个人的所有个人数据。

有关与个人数据一起工作的示例,请参阅PERSONAL_DATA_EXAMPLE.md

待办事项列表

  1. 所有命令都会被记录
  2. 命令/事件具有唯一的ID
  3. 所有其他内容
  4. 企业密钥管理

安装

composer require event-sourcery/event-sourcery

框架支持

Laravel

可以在这里找到Laravel驱动

Symfony

即将推出。

其他?

创建一个问题,特别提出对框架支持的请求。

组件

值表示诸如名称、温度、ID等事物。任何使用值语义的东西都源于以下类。

值语义:相等性是通过比较值来确定的,而不是ID。

  1. SerializableValue
  2. SerializablePersonalDataValue

SerializableValue合约要求您实现用于领域事件序列化的serialize()deserialize()方法。

PersonalData合约标记值为包含符合我们数据保护政策的安全个人数据。(在持久化时加密,并可在符合GDPR的情况下擦除)

可序列化值

Serialization\SerializableValue 接口用于在字符串与值对象之间提供序列化和反序列化功能。该接口被自动领域事件和命令序列化器使用。

任何类型为 stringintbool 或继承自 SerializableValue 的值对象都能够在用户层自动存储和检索,无需自定义事件/命令序列化代码。

作出此决定是因为编写/测试一个值的序列化比测试N次(N为需要编写和单独测试的事件或命令数量)更容易。

<?php
class PersonsName implements SerializableValue {

    /** @var string */
    public $firstName;
    /** @var string */
    public $lastName;

    public function __construct(string $firstName, string $lastName) {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    // Implemented from SerializableValue 
    public function serialize(): string {
        return json_encode([
            'firstName' => $this->firstName,
            'lastName' => $this->lastName,
        ]);
    }

    // Implemented from SerializableValue
    public static function deserialize($string) {
        $values = json_decode($string);
        return new static($values->firstName, $values->lastName);
    }
}

可序列化个人数据值

通过实现 SerializablePersonalDataValue 接口,我们可以将此值对象标记为包含个人数据。

<?php
class Email implements SerializablePersonalDataValue {
    
    /** @var PersonalKey */
    private $personalKey;        
    /** @var string */
    private $address;

    private function __construct(PersonalKey $personalKey, string $address) {
        $this->personalKey = $personalKey;
        $this->address = $address;
        
        if ( ! filter_var($address, FILTER_VALIDATE_EMAIL)) {
            throw new EmailAddressIsNotValid($address);
        }
    }
    
    public static function fromString(PersonalKey $personalKey, string $address) {
        return new static($personalKey, $address);
    }
    
    public function toString() {
        if ($this->wasErased()) {
            throw new CannotRetrieveErasedPersonalData();
        }
        return $this->address;
    }

    // for serialization
    public function serialize(): string {
        return json_encode([
            'personalKey' => $this->personalKey->toString(),
            'address' => $this->address,
        ]);
    }

    public static function deserialize(string $string) {
        $values = json_decode($string);
        return new static(PersonalKey::fromString($values->personalKey), $values->address);
    }
    
    // for supporting erasable data    
    public function personalKey(): PersonalKey {
        return $this->personalKey;
    }
    
    public static function fromErasedState(PersonalKey $personalKey) {
        $email = static($personalKey, "erased-account@mycompany.com");
        $email->erased = true;
        return $email;
    }
    
    public function wasErased() {
        return $this->erased;
    }
    
    private $erased = false;
}

现在,我们的序列化器将知道将数据存储在个人数据存储中,并通过 PersonalDataKey 引用该数据。在反序列化时,它将看到该字段是一个加密值,然后触发从存储中收集加密数据的过程,然后反序列化对象。

实体

实体是以标识符相等性表示的对象。两个对象如果具有相同的ID,则它们相等。如果两个对象的值相同,但ID不同,则它们不比较为相等。如果值完全不同但共享相同的ID,则它们比较为相等。

实体ID

识别是从其他实体中确定一个实体的过程。在这个框架中,ID是以下类之一的子类

  1. Id
  2. StreamId

Id 类存在是为了提供一个基本的ID实现。

StreamId 类是 Id 的子类。它存在是为了区分流ID和其他类型的ID。本质上,它只存在是为了使代码更易于表达/理解。

聚合

聚合是单例过程。功能上一次只能存在一个。这是通过确保每个聚合更新都有一个序列号,并使用RDBMS事务来防止多个进程同时更改聚合来实现的。

聚合的单例性质允许立即一致性。对聚合所做的更新(假设没有违反业务规则并且保持事务性)可以立即确认。

聚合提供公共方法以触发状态变化。这些公共方法将使用内部状态来保护业务规则,然后通过引发新事件来更新聚合。用于保护业务规则的内状态通常来自该聚合内部已经发生的事件。每当在聚合内部发生事件时,其本地状态都会更新,并且该本地状态将被用于保护未来的业务规则。

领域事件

领域事件实现了 DomainEvent 接口。该接口没有任何公共方法。它仅用作标记以区分领域事件。

序列化

包含的基于反射的领域事件序列化器有一些要求。

  1. 它只处理字符串、整数、布尔值以及任何继承自 SerializableValue 的类型。
  2. 所有值都必须注入到构造函数中,并且它们的字段应分配给与示例中相同的名称的字段
<?php
class ValueObjectEventStub implements DomainEvent {

    /** @var ValueObject */
    public $vo;

    public function __construct(ValueObject $vo) {
        $this->vo = $vo;
    }
}

注意,$vo 是构造函数参数,并将其分配给具有相同名称的 $vo 字段。

只要满足这些要求,提供的基于反射的序列化器就能轻松处理它们。它不会区分您是否使用私有字段和getter方法或公共字段。

命令

命令代表了CQRS中的“C”。它们是触发某些有限系统状态变化或外部副作用的行为。您自己选择。

此示例说明了框架的习惯用法

<?php
class RegisterCandidate implements Command {

    /** @var VoucherId */
    private $voucherId;
    /** @var CandidateName */
    private $candidateName;
    /** @var Email */
    private $candidateEmail;
    /** @var Timestamp */
    private $registerAt;

    public function __construct(string $voucherId, string $candidateName, string $candidateEmail, string $registerAt) {
        $this->voucherId = VoucherId::fromString($voucherId);
        $this->candidateName = CandidateName::fromString($candidateName);
        $this->candidateEmail = Email::fromString($candidateEmail);
        $this->registerAt = Timestamp::fromString($registerAt);
    }

    public function execute(EventStore $events) {
        $candidate = Candidate::buildFrom($events->getStream($this->voucherId));
        $candidate->doSomething();
        $events->storeStream($candidate->flushEvents());
    }
}

此示例包含两个方法;构造函数和 execute()。

constructor 用于将原始数据(在UI中收集)转换为领域对象。

《execute》方法实际上实现了命令的行为。在execute()方法中不需要任何参数。但是,如果您对参数进行了类型提示,框架将从PSR-11兼容容器中解析这些参数,以便解析并将依赖自动注入到《execute》方法中。

《execute》方法是由命令总线调用的。

以下是一个示例控制器方法

<?php
class ExampleController {

    public function __construct(CommandBus $bus) {
        $this->bus = $bus;
    } 

    public function controllerMethod(Request $request) {
        $this->bus->execute(new RegisterCandidate(
            $request->get('voucherId'),
            $request->get('candidateName'),
            $request->get('candidateEmail'),
            "2017-01-02 03:04:05"
        ));
    }
}

命令总线负责提供任何数量的功能,并调用《execute()》方法。提供的实现使用了一个PSR-11兼容的服务容器

事件存储

事件存储的职责是存储和查询事件。我们实现的核心是《EventStore》接口。从这里,您可以按任何方式实现存储。

可能提供了一个关系数据库管理系统(RDBMS)实现。提供的实现存储事件,然后通过事件调度器分派事件。

事件分发和监听器

事件分派是将新存储的事件传递给事件监听器的过程。

事件监听器是任何对新生成事件做出反应的组件。

如聚合事件重放等行为不会触发事件监听器,因为这些事件在最初存储时已经传递给监听器一次。监听器不会接收到同一事件多次。

进程管理器

进程管理器被视为一种特殊类型的事件监听器,因为它们有可能通过引发事件或执行导致新事件的结果的命令直接修改系统的状态。

投影

投影是对事件流的解释。

个人数据安全

个人数据安全是这个库的核心价值观。然而,个人安全并不是可以通过自动方式实现的东西。

我们认为我们有责任首先为人民服务他们的权利,而不是追求经济利益。我们正在以寻求最大限度地尊重他们隐私神圣性的方式构建这个库。

为了最好地保护个人隐私,我们使用以下约定

  1. 事件存储中不存储任何个人数据。相反,它存储在《个人数据存储》中。
  2. 不存储未加密的个人数据。个人数据存储中的数据使用存在于单独的《个人加密存储》中的密钥进行加密。
  3. 所有利用个人数据的系统都必须在以下预期下构建:个人数据可能需要根据GDPR进行删除,并且系统/应用程序必须保持功能。
  4. 我们鼓励即使在欧盟以外运营的开发者也要遵守GDPR。

我们相信维护《数据保护法》和《通用数据保护条例合规》的价值观。

个人密钥

个人密钥是一个唯一密钥,用于标识单个《人员》。《人员》是指存储有个人识别数据且在调用《删除权》时需要被遗忘的个体。

通常,个人密钥会通过ID构建,例如《PersonalKey::fromId($memberId)》。“个人密钥”与个人数据一起存储,以便我们可以确定个人数据属于谁。

个人数据存储

个人数据存储是应用程序中唯一保存个人数据的位置。这种存储可以以任何方式进行实现。您的实现必须实现 PersonalDataStore 接口。

个人加密存储

在我们的实现中,所有个人数据始终被加密。加密密钥存储在一个名为 Personal Cryptography Store 的独立存储中。这种存储可以以任何方式进行实现。您的实现必须实现 PersonalCryptographyStore 接口。

个人数据和加密密钥存储的最佳实践

建议确保,如果有人获取以下三者之一,他们将无法访问个人数据。

  1. 您的 Personal Data Store 内容。
  2. 您的 Personal Cryptography Store 内容。
  3. 您的源代码。

以下提示可以帮助确保这一点

  1. 不要在源代码中存储指向个人数据存储或个人加密存储的地址或身份验证信息。
  2. 确保您的存储位于与源代码和彼此隔离的系统中。
  3. 确保您的存储通过加密通道安全地链接到源代码(或需要访问的系统)。在任何时候都不应破坏防火墙,以便系统可以访问您的存储。

驱动实现

存储的实现将以驱动程序的形式提供。

当使用驱动程序时,使用 composer require 驱动程序而不是此包。

加密密钥

首次为 PersonalKey 存储个人数据时,将分配 cryptographic detailsPersonal Key。可以通过查询 Personal Cryptography Store 来获取 cryptographic details。这些 cryptographic details 应该永不缓存,而应始终从 Personal Cryptography Store 查询。

这些 cryptographic details 用于解密私人个人数据。

注释

  1. Serialize() 和 Deserialize() 与 toString() 和 fromString() 不同。前一种方法专门用于序列化。后一种用于利用字符串作为实现或输出(例如读取模型或异常)以及个别实现可能需要不同的。
  2. 序列化方法需要返回和接受数组,因为这允许最终进入事件存储的 json 编码序列化形式是一个大型的 json 对象。如果另一个进程接收此对象,它只需进行 json 解码并分解即可,而无需知道特定字段是一个已转义的 json 编码字符串,该字符串位于 json 对象定义中。