sap采购组织和采购组_捏事件采购

sap采购组织和采购组

Let’s talk about Event Sourcing. Perhaps you’ve heard of it, but haven’t found the time to attend a conference talk or read one of the older, larger books which describe it. It’s one of those topics I wish I’d known about sooner, and today I’m going to describe it to you in a way that I understand it.

让我们谈谈事件采购。 也许您已经听说过它,但是却没有时间参加会议演讲或阅读描述它的较旧的 较大书籍之一。 这是我希望早日知道的主题之一,今天,我将以一种理解的方式向您描述。

Most of this code can be found on GitHub. I’ve tested it using PHP 7.1.

大部分代码可以在GitHub找到 我已经使用PHP 7.1对其进行了测试。

Sequence of vertical columns of different colors, each connected to the next with an overlapping error, symbolizing evolution, progress, or versioning

I’ve chosen this title for a few reasons. Firstly, I don’t consider myself an expert on the topic. For that, you’d be hard pressed to find a better tutor than the authors of those books, or someone like Mathias Verraes. What I’m about to tell you is only the tip of the iceberg. A pinch of salt, if you will.

我之所以选择该标题是出于几个原因。 首先,我不认为自己是该主题的专家。 为此,很难找到比那些书的作者或Mathias Verraes这样的人更好的导师。 我要告诉你的只是冰山一角。 如果需要,可以加少许盐。

Event sourcing is also part of a larger, broader set of topics; loosely defined as Domain Driven Design. Event sourcing is one design pattern amongst many, and you’d do well to learn about the other patterns associated with DDD. In fact, it’s often not a good idea to pluck just Event Sourcing out of the DDD toolbox, without understanding the benefits of the other patterns.

事件源也是更大,更广泛主题集的一部分; 松散地定义为域驱动设计。 事件源是许多设计模式中的一种,您最好了解与DDD相关的其他模式。 实际上,在不了解其他模式的好处的情况下,仅从DDD工具箱中抽出Event Sourcing通常不是一个好主意。

Still, I think it’s a fascinating and fun exercise, and few people cover it well. It’s especially suited for those developers who have yet to dip their toes in the pool of DDD. So, if you find yourself needing something like Event Sourcing, but don’t know or understand the rest of DDD, I hope this post helps you. In a pinch.

不过,我认为这是一个有趣而有趣的练习,很少有人能很好地完成它。 它特别适合那些还没有在DDD中投入精力的开发人员。 因此,如果您发现自己需要诸如事件源之类的东西,但不了解或不了解DDD的其余部分,希望这篇文章对您有所帮助。 紧要关头

通用语言 (Common Language)

One of the strongest themes of Domain Driven Design is the need for a common language. When your client decides they need a new application, they are thinking about how it will affect their ice-cream sales. They’re concerned about how their patrons will find their favorite flavor of ice-cream, and how that will affect foot-traffic at their ice-cream stand.

域驱动设计的最强主题之一是对通用语言的需求。 当您的客户决定需要新的应用程序时,他们正在考虑它将如何影响他们的冰淇淋销售。 他们担心他们的顾客如何找到自己喜欢的冰淇淋口味,以及这将如何影响他们在冰淇淋摊位上的脚步。

You may think in terms of website users and geolocated outlets, but those words don’t necessarily mean anything to your client. Though it may take some time, initially, your communication with your client will be greatly improved if you both use the same words when talking about the same thing.

您可能会从网站用户和地理位置的角度来思考,但是这些词对您的客户不一定有任何意义。 尽管可能会花费一些时间,但最初,如果您在谈论同一件事时都使用相同的词语,则与您的客户的沟通将大大改善。

You’ll also find that modeling the entire system in the words your client understands gives you a bit of a safety net against scope changes. It’s much easier to say; “You initially asked for customers to purchase ice-cream before the invoice is sent (shown here in code and email), but now you’re asking for the invoice to be sent first…” than it is to describe the changes they’re asking for in language/code only you understand.

您还将发现,以客户理解的语言对整个系统进行建模可以使您免受范围变更的影响。 说起来容易得多; “您最初要求客户在发送发票之前先购买冰淇淋(在代码和电子邮件中显示),但现在您要先发送发票...”而不是描述他们要进行的更改。仅以您了解的语言/代码询问。

That’s not to say all your code needs to be understood by the client, or that you have to use something like Behat for your integration testing. But, at the very least, you should call entities and actions the same thing as your client does.

这并不是说您的所有代码都需要客户端理解,或者您必须使用Behat之类的工具进行集成测试。 但是,至少,您应该将实体和动作称为与客户相同的东西。

An added benefit of this is that future developers will be able to understand the intent of the code (and how it applies to the business process), without as much help from the client or project manager.

这样做的另一个好处是,将来的开发人员将能够理解代码的意图(以及代码如何应用于业务流程),而无需客户或项目经理的帮助。

I’m waffling a bit, but this point will be important when we start to write code.

我有点费劲,但是当我们开始编写代码时,这一点很重要。

存储状态与存储行为 (Storing State vs. Storing Behavior)

Most of the websites I’ve built have had some form of CRUD (Create, Read, Update, and Delete) database functionality. These operations are intentionally generic, as they have traditionally mapped to the underlying relational database they use.

我建立的大多数网站都具有某种形式的CRUD(创建,读取,更新和删除)数据库功能。 这些操作是有意通用的,因为它们传统上已映射到它们使用的基础关系数据库。

储存状态 (Storing State)

We may even be used to using something like Eloquent:

我们甚至可能习惯于使用Eloquent之类的东西:

$product = new Product();
$product->title = "Chocolate";
$product->cents_per_serving = 499;
$product->save();

$outlet = new Outlet();
$outlet->location = "Pismo Beach";
$outlet->save();

$outlet->products()->sync([
    $product->id => [
        "servings_in_stock" => 24,
    ],
])

This is enough for the most basic presentation of ice-cream information on the client’s website. It’s how we’ve been building websites for ages. But it has a significant weakness — we don’t know what happened to get us here.

这足以在客户网站上最基本地呈现冰淇淋信息。 这就是我们多年来建立网站的方式。 但是它有一个明显的弱点-我们不知道发生了什么事而使我们来到这里。

Let’s think of some things which could influence how the data got to this point:

让我们考虑一些可能影响数据到这一点的事情:

  • When did we start selling “Chocolate”? Many Object Relation Mappers (ORM) libraries will add fields like created_at and updated_at, but those only go so far in telling us what we want to know.

    我们什么时候开始销售“巧克力”? 许多对象关系映射器(ORM)库都会添加诸如created_atupdated_at类的created_at ,但这些created_at仅在告诉我们我们想知道的内容方面走得很远。

  • How did we get that much stock? Did we get a delivery? Did we give some away?

    我们如何得到这么多库存? 我们收到货了吗? 我们有送些东西吗?
  • What happens to our analytics when we no longer want to sell “Chocolate”, or when we want to move all stock to another outlet? Do we add a boolean field (to the products’ table), to indicate that the product is no longer sold, but should remain in the analytics? Or perhaps we should add a timestamp, so we know when that all happened…

    当我们不再想要出售“巧克力”或想要将所有库存转移到另一家商店时,我们的分析会发生什么? 我们是否在产品表中添加一个布尔值字段以指示该产品已不再销售,但应保留在分析中? 或者,也许我们应该添加一个时间戳记,以便我们知道什么时候发生了……

储存行为 (Storing Behavior)

The weakness is such that we only know what the data is like now. Our data is like a photo, when what we want is a video. What if we tried something different?

弱点在于我们只知道现在的数据是什么样的。 当我们想要的是视频时,我们的数据就像一张照片。 如果我们尝试不同的方法怎么办?

$events = [];

$events[] = new ProductInvented("Chocolate");
$events[] = new ProductPriced("Chocolate", 499);
$events[] = new OutletOpened("Pismo Beach");
$events[] = new OutletStocked("Pismo Beach", 24, "Chocolate");

store($events);

This is storing the same eventual information, but each of the steps is self-contained. They describe the behavior of the customers, outlets, stock etc.

这将存储相同的最终信息,但是每个步骤都是独立的。 它们描述了客户,网点,库存等的行为。

Using this approach, we have much better control over the timeline of events which have lead to the current state. We could add events for stock giveaways, or product discontinuation:

使用这种方法,我们可以更好地控制导致当前状态的事件的时间轴。 我们可以为库存赠品或产品停产添加事件:

$events = [];

$events[] = new OutletStockGivenAway(
    "Pismo Beach", 2, "Chocolate"
);

$events[] = new OutletDiscontinuedProduct(
    "Pismo Beach", "Chocolate"
);

store($events);

This isn’t more complex than storing state, but it is far more descriptive of the events that happen. It’s also really easy for the client to understand what’s going on.

这并不比存储状态复杂,但是它更能描述发生的事件。 客户也很容易了解发生了什么。

When we start to store behavior (instead of the state at one point in time), we gain the ability to easily step through the events. Almost like we’re traveling through time:

当我们开始存储行为(而不是某个时间点的状态)时,我们将获得轻松处理事件的能力。 就像我们穿越时空:

$lastWeek = Product::at("Chocolate", date("-1 WEEK"));
$yesterday = Product::at("Chocolate", date("-1 DAY"));

printf(
    "Chocolate increased, from %s to %s, in one week",
    $lastWeek->cents_per_serving,
    $yesterday->cents_per_serving
);

… and we could do that without any extra boolean/timestamp fields. We could come back to already-stored data, and create a new kind of report. That’s so valuable!

…并且我们可以在没有任何额外的布尔/时间戳字段的情况下做到这一点。 我们可以返回到已经存储的数据,并创建一种新的报告。 太有价值了!

那是什么呢? (So Which Is It?)

Event Sourcing is both of these things. It’s about capturing every event (which you can think of as every change in application data) as a self-contained, repeatable thing. It’s about storing these events in the same time-order they happened, so that we can at-will journey to any point in time.

事件采购是这两个方面。 这是关于将每个事件(您可以将其视为应用程序数据中的每个更改)捕获为一个独立的,可重复的事情。 这是关于将这些事件按发生的相同时间顺序进行存储,以便我们随时随地到达任何时间点。

It’s about understanding how to interface this architecture with other systems that aren’t built in the same way, which means having a way to represent just the latest application data state.

这是关于了解如何将该体系结构与不是以相同方式构建的其他系统接口,这意味着有一种方法可以仅表示最新的应用程序数据状态。

The events are append-only, which means we never delete any of them from the database. And, if we’re doing things right, they describe (in their names and properties) what they mean to the business and customer they relate to.

这些事件仅是追加事件,这意味着我们绝不会从数据库中删除任何事件。 而且,如果我们做对了,他们会(在其名称和属性中)描述其对与之相关的业务和客户的意义。

进行活动 (Making Events)

We’re going to use classes to describe events. They’re useful, simple containers we can define; and they’ll help us validate the data we put in and the data we get out for each event.

我们将使用类来描述事件。 它们是我们可以定义的有用,简单的容器; 它们将帮助我们针对每个事件验证输入的数据和取出的数据。

Those experienced in Event Sourcing may be itching to hear how I describe things like aggregates. I’m intentionally avoiding jargon — in much the same way as I’d avoid differentiating between mocks, doubles, stubs, and fakes — if I were teaching someone their first bit of testing. It’s the idea that is important, and the idea behind Event Sourcing is recording behavior.

那些具有事件采购经验的人可能会很想听到我如何描述聚合之类的东西。 如果我要教别人第一个测试位,我有意避免使用行话-就像避免区分模拟,双打,存根和伪造品一样。 这是很重要的想法 ,和事件背后的采购思路是记录的行为。

Here’s the abstract event that we can use to model real events:

这是我们可以用来对真实事件建模的抽象事件:

abstract class Event
{
    /**
     * @var DateTimeImmutable
     */
    private $date;

    protected function __construct()
    {
        $this->date = date("Y-m-d H:i:s");
    }

    public function date(): string
    {
        return $this->date;
    }

    abstract public function payload(): array;
}

This is from events.php

这是来自events.php

It’s really important (in my opinion) that event classes are simple. Using PHP 7 type hints, we can validate the data we use to define events. A handful of simple accessors will help us get the important data out again.

(在我看来)事件类很简单是非常重要的。 使用PHP 7类型提示,我们可以验证用于定义事件的数据。 少数简单的访问器将帮助我们再次获取重要数据。

On top of this class, we can define the real event types we want to record:

在此类的顶部,我们可以定义要记录的真实事件类型:

final class ProductInvented extends Event
{
    /**
     * @var string
     */
    private $name;

    public function __construct(string $name)
    {
        parent::__construct();

        $this->name = $name;
    }

    public function payload(): array
    {
        return [
            "name" => $this->name,
            "date" => $this->date(),
        ];
    }
}
final class ProductPriced extends Event
{
    /**
     * @var string
     */
    private $product;

    /**
     * @var int
     */
    private $cents;

    public function __construct(string $product, int $cents)
    {
        parent::__construct();

        $this->product = $product;
        $this->cents = $cents;
    }

    public function payload(): array
    {
        return [
            "product" => $this->product,
            "cents" => $this->cents,
            "date" => $this->date(),
        ];
    }
}
final class OutletOpened extends Event
{
    /**
     * @var string
     */
    private $name;

    public function __construct(string $name)
    {
        parent::__construct();

        $this->name = $name;
    }

    public function payload(): array
    {
        return [
            "name" => $this->name,
            "date" => $this->date(),
        ];
    }
}
final class OutletStocked extends Event
{
    /**
     * @var string
     */
    private $outlet;

    /**
     * @var int
     */
    private $servings;

    /**
     * @var string
     */
    private $product;

    public function __construct(string $outlet, ↩
        int $servings, string $product)
    {
        parent::__construct();

        $this->outlet = $outlet;
        $this->servings = $servings;
        $this->product = $product;
    }

    public function payload(): array
    {
        return [
            "outlet" => $this->outlet,
            "servings" => $this->servings,
            "product" => $this->product,
            "date" => $this->date(),
        ];
    }
}

Notice how we’ve made each of these final? We have to fight to keep the events simple, and they wouldn’t continue to be simple if another developer could come along and subclass them (for whatever reason).

注意我们如何完成所有这些final ? 我们必须努力使事件保持简单,并且如果其他开发人员可以随便对它们进行子类化(无论出于何种原因),它们就不会继续保持简单。

I also find it interesting how we can isolate the definition, format, and accessibility of the event dates: by defining $date as private and requiring subclasses to access it through the date method. This is perhaps a tad too defensive, but it obeys the Law of Demeter in that the concrete events need not know how the date is defined or formatted, in order to use it.

我还发现有趣的是我们如何隔离事件日期的定义,格式和可访问性:通过将$date定义为private并要求子类通过date方法访问它。 这也许有点防御性,但是它遵循了得墨meter耳定律 ,因为具体事件不需要知道如何定义或格式化日期就可以使用它。

With this isolation, we can change the entire system’s timezone, or change to using UNIX timestamps, and we’d only need to change a single line of code.

通过这种隔离,我们可以更改整个系统的时区,也可以更改为使用UNIX时间戳,并且只需要更改一行代码即可。

We could omit these classes if we’re willing to sacrifice performance (and do runtime associative array checks) or type safety.

如果我们愿意牺牲性能(并进行运行时关联数组检查)或类型安全,则可以省略这些类。

储存事件 (Storing Events)

Let’s store these events in a SQLite database. We could use an ORM for that, but perhaps this is a good opportunity to recap how PDO works.

让我们将这些事件存储在SQLite数据库中。 我们可以为此使用ORM,但是也许这是一个概述PDO工作原理的好机会。

使用PDO (Using PDO)

The first bit of code, for connecting to any supported database through PDO, is:

通过PDO连接到任何受支持的数据库的第一行代码是:

$connection = new PDO("sqlite::memory:");

$connection->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION
);

This is from sqlite-pdo.php

这是来自sqlite-pdo.php

PDO connections are typically made using a Data Source Name (DSN). Here we define the database type as sqlite, and the location as an in-memory database. This means the database will disappear as soon as the script finishes.

PDO连接通常使用数据源名称(DSN)建立。 在这里,我们将数据库类型定义为sqlite ,并将位置定义为内存数据库。 这意味着脚本完成后,数据库将消失。

It’s also a good idea to set the error-mode to throw exceptions when a SQL error occurs. That way we’ll get immediate feedback on our mistakes.

设置错误模式以在发生SQL错误时引发异常也是一个好主意。 这样,我们将立即获得关于错误的反馈。

If you’ve not done any raw SQL queries before, this next bit may be confusing. Check out this great introduction to SQL.

如果您之前未进行过任何原始SQL查询,那么下一点可能会造成混淆。 查阅有关SQL的出色介绍

Next, we should create some tables to work from:

接下来,我们应该创建一些表以进行工作:

$statement = $connection->prepare("
    CREATE TABLE IF NOT EXISTS product (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT
    )
");

$statement->execute();

This is from sqlite-pdo.php

这是来自sqlite-pdo.php

One of those tables is going to be where we generate and store unique product identifiers. The exact syntax of CREATE TABLE differs slightly between database types, and you’d typically find more columns in a table.

这些表之一将是我们生成和存储唯一产品标识符的地方。 CREATE TABLE的确切语法在数据库类型之间略有不同,并且通常会在表中找到更多列。

A great way to learn how your database creates tables is to make a table through a GUI, and then run SHOW CREATE TABLE my_new_table. This will generate CREATE TABLE syntax, in all of PDO’s supported databases.

了解数据库如何创建表的一种好方法是通过GUI SHOW CREATE TABLE my_new_table ,然后运行SHOW CREATE TABLE my_new_table 这将在所有PDO支持的数据库中生成CREATE TABLE语法。

Prepared statements (using prepare and execute) are the recommended way of executing SQL queries. They are even more useful when you need to pass query parameters:

建议的语句(使用prepareexecute )是执行SQL查询的推荐方法。 当您需要传递查询参数时,它们甚至更有用:

$statement = $connection->prepare(
    "INSERT INTO product (name) VALUES (:name)"
);

$statement->bindValue("name", "Chocolate");
$statement->execute();

This is from sqlite-pdo.php

这是来自sqlite-pdo.php

Bound values are automatically quoted and escaped, avoiding the most common kinds of SQL injection. We can also use prepared statements to return rows:

绑定值会自动加引号并转义,从而避免了最常见SQL注入。 我们还可以使用准备好的语句返回行:

$row = $connection
    ->prepare("SELECT * FROM product")
    ->execute()->fetch(PDO::FETCH_ASSOC);

$rows = $connection
    ->prepare("SELECT * FROM product")
    ->execute()->fetchAll(PDO::FETCH_ASSOC);

This is from sqlite-pdo.php

这是来自sqlite-pdo.php

These fetch and fetchAll methods will return arrays and arrays or arrays, respectively, given that we’re using the PDO::FETCH_ASSOC type.

假设我们使用的是PDO::FETCH_ASSOC类型,则这些fetchfetchAll方法将分别返回数组和一个或多个数组。

添加助手功能 (Adding Helper Functions)

As you can probably guess, using PDO directly can lead to a lot of needless repetition. I’ve found it useful to create a few helper functions:

您可能会猜到,直接使用PDO会导致很多不必要的重复。 我发现创建一些辅助函数很有用:

function connect(string $dsn): PDO
{
    $connection = new PDO($dsn);

    $connection->setAttribute(
        PDO::ATTR_ERRMODE,
        PDO::ERRMODE_EXCEPTION
    );

    return $connection;
}

function execute(PDO $connection, string $query, ↩
    array $bindings = []): array
{
    $statement = $connection->prepare($query);

    foreach ($bindings as $key => $value) {
        $statement->bindValue($key, $value);
    }

    $result = $statement->execute();

    return [$statement, $result];
}

function rows(PDO $connection, string $query, ↩
    array $bindings = []): array
{
    $executed = execute($connection, $query, $bindings);

    /** @var PDOStatement $statement */
    $statement = $executed[0];

    return $statement->fetchAll(PDO::FETCH_ASSOC);
}

function row(PDO $connection, string $query, ↩
    array $bindings = []): array
{
    $executed = execute($connection, $query, $bindings);

    /** @var PDOStatement $statement */
    $statement = $executed[0];

    return $statement->fetch(PDO::FETCH_ASSOC);
}

This is from sqlite-pdo-helpers.php

这来自sqlite-pdo-helpers.php

This are much the same code as we saw before. They’re a little nicer to use than the direct PDO code though:

这和我们之前看到的代码大致相同。 但是,它们比直接的PDO代码好用一点:

$connection = connect("sqlite::memory:");

execute(
    $connection,
    "CREATE TABLE IF NOT EXISTS product ↩
        (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT)"
);

execute(
    $connection,
    "INSERT INTO product (name) VALUES (:name)",
    ["name" => "Chocolate"]
);

$rows = rows(
    $connection,
    "SELECT * FROM product"
);

$row = row(
    $connection,
    "SELECT * FROM product WHERE name = :name",
    ["name" => "Chocolate"]
);

This is from sqlite-pdo-helpers.php

这来自sqlite-pdo-helpers.php

You may not like the idea of defining global functions for these things. They’re like something you’d see in the dark ages of PHP. But they’re so concise, and easy to use!

您可能不喜欢为这些事情定义全局功能的想法。 它们就像您在PHP的黑暗时代看到的一样。 但是它们是如此的简洁,并且易于使用!

They’re not even difficult to test:

他们甚至都不难测试:

$fake = new class("sqlite::memory:") extends PDO
{
    private $valid = true;

    function prepare($statement, $options = null) {
        if ($statement !== "SELECT * FROM product") {
            $this->valid = false;
        }

        return $this;
    }

    function execute() {
        return;
    }

    function fetchAll() {
        if (!$this->valid) {
            throw new Exception();
        }

        return [];
    }
};

assert(connect("sqlite::memory:") instanceof PDO);
assert(is_array(rows($fake, "SELECT * FROM product")));

This is from sqlite-pdo-helpers.php

这来自sqlite-pdo-helpers.php

We’re not testing all the variations, of the helper functions, but you get the idea…

我们没有测试辅助功能的所有变体,但是您知道了…

If you’re still confused, for a more in-depth look at PDO, see this post.

如果您仍然感到困惑,那么对于PDO的更深入的了解,请参阅这篇文章

储存事件 (Storing Events)

Let’s take another look at the events we want to store:

让我们再看一下我们要存储的事件:

$events = [];

$events[] = new ProductInvented("Chocolate");
$events[] = new ProductPriced("Chocolate", 499);
$events[] = new OutletOpened("Pismo Beach");
$events[] = new OutletStocked("Pismo Beach", 24, "Chocolate");

The simplest approach would be to create a database table for each of these event types:

最简单的方法是为以下每种事件类型创建一个数据库表:

execute($connection, "
    CREATE TABLE IF NOT EXISTS product (
        id INTEGER PRIMARY KEY AUTOINCREMENT
    )
");

execute($connection, "
    CREATE TABLE IF NOT EXISTS event_product_invented (
        id INT,
        name TEXT,
        date TEXT
    )
");

execute($connection, "
    CREATE TABLE IF NOT EXISTS event_product_priced (
        product INT,
        cents INT,
        date TEXT
    )
");

execute($connection, "
    CREATE TABLE IF NOT EXISTS outlet (
        id INTEGER PRIMARY KEY AUTOINCREMENT
    )
");

execute($connection, "
    CREATE TABLE IF NOT EXISTS event_outlet_opened (
        id INT,
        name TEXT,
        date TEXT
    )
");

execute($connection, "
    CREATE TABLE IF NOT EXISTS event_outlet_stocked (
        outlet INT,
        servings INT,
        product INT,
        date TEXT
    )
");

This is from storing-events.php

这是来自storing-events.php

In addition to a table for each event, I’ve also added tables to store and generate product and outlet IDs. Each event table has a date field, the value of which is generated by the abstract Event class.

除了每个事件的表外,我还添加了一些表来存储和生成产品和商店ID。 每个事件表都有一个日期字段,其值由抽象Event类生成。

The real magic happens in the store and storeOne functions:

真正的魔力发生在storestoreOne函数中:

function store(PDO $connection, array $events)
{
    foreach($events as $event) {
        storeOne($connection, $event);
    }
}

function storeOne(PDO $connection, Event $event)
{
    $payload = $event->payload();

    if ($event instanceof ProductInvented) {
        inventProduct(
            $connection,
            newProductId($connection),
            $payload["name"],
            $payload["date"]
        );
    }

    if ($event instanceof ProductPriced) {
        priceProduct(
            $connection,
            productIdFromName($connection, $payload["name"]),
            $payload["cents"],
            $payload["date"]
        );
    }

    if ($event instanceof OutletOpened) {
        openOutlet(
            $connection,
            newOutletId($connection),
            $payload["name"],
            $payload["date"]
        );
    }

    if ($event instanceof OutletStocked) {
        stockOutlet(
            $connection,
            outletIdFromName(
                $connection, $payload["outlet_id"]
            ),
            $payload["servings"],
            productIdFromName(
                $connection, $payload["product_id"]
            ),
            $payload["date"]
        );
    }
}

This is from storing-events.php

这是来自storing-events.php

The store function is just a convenience. PHP has no concept of typed arrays, so we could add runtime checking, or use the signature of storeOne to validate that we’re only trying to store Event subclass instances.

store功能只是一种便利。 PHP没有类型数组的概念,因此我们可以添加运行时检查,或使用storeOne的签名来验证我们仅尝试存储Event子类实例。

We can get specific event data via the payload method. This data will differ based on the event class being stored, so we should only assume keys after we’re sure which event type we’re dealing with.

我们可以通过payload方法获取特定的事件数据。 这些数据将根据存储的事件类而有所不同,因此我们仅应在确定要处理的事件类型后才使用键。

We’re also using some product and outlet helper methods. Here’s what they look like:

我们还使用了一些产品和渠道帮助方法。 它们是这样的:

function newProductId(PDO $connection): int
{
    execute(
        $connection,
        "INSERT INTO product VALUES (null)"
    );

    return $connection->lastInsertId();
}

function inventProduct(PDO $connection, int $id, ↩
    string $name, string $date)
{
    execute(
        $connection,
        "INSERT INTO event_product_invented ↩
            (id, name, date) VALUES (:id, :name, :date)",
        ["id" => $id, "name" => $name, "date" => $date]
    );
}

function productIdFromName(PDO $connection, string $name): int
{
    $row = row(
        $connection,
        "SELECT * FROM event_product_invented ↩
            WHERE name = :name",
        ["name" => $name]
    );

    if (!$row) {
        throw new InvalidArgumentException("Product not found");
    }

    return $row["id"];
}

function priceProduct(PDO $connection, int $product, ↩
    int $cents, string $date)
{
    execute(
        $connection,
        "INSERT INTO event_product_priced ↩
            (product, cents, date) VALUES ↩
            (:product, :cents, :date)",
        ["product" => $product, "cents" => $cents, ↩
            "date" => $date]
    );
}

function newOutletId(PDO $connection): int
{
    execute(
        $connection,
        "INSERT INTO outlet VALUES (null)"
    );

    return $connection->lastInsertId();
}

function openOutlet(PDO $connection, int $id, ↩
    string $name, string $date)
{
    execute(
        $connection,
        "INSERT INTO event_outlet_opened (id, name, date) ↩
            VALUES (:id, :name, :date)",
        ["id" => $id, "name" => $name, "date" => $date]
    );
}

function outletIdFromName(PDO $connection, string $name): int
{
    $row = row(
        $connection,
        "SELECT * FROM event_outlet_opened ↩
            WHERE name = :name",
        ["name" => $name]
    );

    if (!$row) {
        throw new InvalidArgumentException("Outlet not found");
    }

    return $row["id"];
}

function stockOutlet(PDO $connection, int $outlet, ↩
    int $servings, int $product, string $date)
{
    execute(
        $connection,
        "INSERT INTO event_outlet_stocked ↩
            (outlet_id, servings, product_id, date) ↩
            VALUES (:outlet, :servings, :product, :date)",
        ["outlet" => $outlet, "servings" => $servings, ↩
            "product" => $product, "date" => $date]
    );
}

This is from storing-events.php

这是来自storing-events.php

inventProduct, priceProduct, openOutlet, and stockOutlet are all pretty self-explanatory. In order to get the IDs they refer to, we need the newProductId and newOutletId functions. These insert empty rows so that unique identifiers will be generated and can be returned (using the $connection->lastInsertId() method).

inventProductpriceProductopenOutletstockOutlet都是不言自明。 为了获得它们引用的ID,我们需要newProductIdnewOutletId函数。 它们插入空行,以便生成唯一标识符并可以将其返回(使用$connection->lastInsertId()方法)。

You do not have to follow this same naming pattern. In fact, it’s better to use names and patterns that you and your client agree define the core concepts of the product, as far as DDD is concerned.

您不必遵循相同的命名模式。 实际上,就DDD而言,最好使用您和客户都同意的名称和模式来定义产品的核心概念。

We can test these using a pattern similar to:

我们可以使用类似以下的模式来测试它们:

store($connection, [
    new ProductInvented("Cheesecake"),
]);

$row = row(
    $connection,
    "SELECT * FROM event_product_invented WHERE name = :name",
    ["name" => "Cheesecake"]
);

assert(!is_null($row));

This is from storing-events.php

这是来自storing-events.php

投影活动 (Projecting Events)

As we’ve seen, the method of storing behavior gives us an unprecedented look at the entire history of our data. It’s not very good for rendering views, though. As I mentioned, we also need a way to interface an event sourcing architecture with other systems that are not built in the same way.

如我们所见,存储行为的方法使我们对数据的整个历史有了前所未有的了解。 但是,这对于渲染视图不是很好。 正如我所提到的,我们还需要一种将事件源架构与其他系统构建方式不同的接口。

That means we need to be able to tell the outside world what the more recent state of the application is, as if we were storing it like that in the database. This is often called projection, because we sort through all the events to display a final state for everyone else to see. So, projection in the sense of forecasting a future state, based on present trends.

这意味着我们需要能够告诉外界应用程序的最新状态是什么,就像我们将其存储在数据库中一样。 这通常称为投影,因为我们对所有事件进行分类以显示最终状态,以供其他所有人查看。 因此, 从当前趋势的角度预测未来状态

I believe one of the biggest hurdles to Event Sourcing newcomers face is not knowing how to realistically apply it to their situation. It doesn’t help we talk about the theory of Event Sourcing if we don’t talk about how to use it well!

我认为,新人面临的最大事件采购障碍是不知道如何切实地将其应用于他们的情况。 如果我们没有很好地讨论如何使用事件源理论,那对我们没有帮助。

Earlier we saw functions like:

之前我们看到的功能如下:

Product::at("Chocolate", date("-1 WEEK"));
// → ["id" => 1, "name" => "Chocolate", ...]

Ideally, we’d also have these methods:

理想情况下,我们还将具有以下方法:

Product::latest();
// → [["id" => 1, "name" => "Chocolate", ...], ...]

Product::latest("Chocolate");
// → ["id" => 1, "name" => "Chocolate", ...]

First, we need to load all the events stored in the database:

首先,我们需要加载存储在数据库中的所有事件:

function fetch(PDO $connection): array {
    $events = [];

    $tables = [
        ProductInvented::class => "event_product_invented",
        ProductPriced::class => "event_product_priced",
        OutletOpened::class => "event_outlet_opened",
        OutletStocked::class => "event_outlet_stocked",
    ];

    foreach ($tables as $type => $table) {
        $rows = rows($connection, "SELECT * FROM {$table}");

        $rows = array_map(
            function($row) use ($connection, $type) {
                return $type::from($connection, $row);
            }, $rows
        );

        $events = array_merge($events, $rows);
    }

    usort($events, function(Event $a, Event $b) {
        return strtotime($a->date()) - strtotime($b->date());
    });

    return $events;
}

This is from projecting-events.php

这是来自projecting-events.php

There’s quite a bit going on here, so let’s break it down:

这里有很多事情,所以让我们分解一下:

  1. We define a list of event tables to get rows from.

    我们定义事件表列表以获取行。
  2. We fetch the rows for each type/table, and convert the resulting associative arrays to instances of the events.

    我们获取每种类型/表的行,并将结果关联数组转换为事件的实例。
  3. We use the date of each event, to sort them into chronological order.

    我们使用每个事件的date ,将它们按时间顺序排序。

We need to add these new from methods to each of our events:

我们需要将这些新的from方法添加到每个事件中:

abstract class Event
{
    // ...snip

    public function withDate(string $date): self
    {
        $new = clone $this;
        $new->date = $date;

        return $new;
    }

    abstract
    public
    static
    function
    from(PDO $connection, array $data);
}
final class ProductInvented extends Event
{
    // ...snip

    public static function from(PDO $connection, array $data)
    {
        $new = new static(
            $data["name"]
        );

        return $new->withDate($data["date"]);
    }
}
final class ProductPriced extends Event
{
    // ...snip

    public static function from(PDO $connection, array $data)
    {
        $new = new static(
            productNameFromId($connection, $data["product"]),
            $data["cents"]
        );

        return $new->withDate($data["date"]);
    }
}
final class OutletOpened extends Event
{
    // ...snip

    public static function from(PDO $connection, array $data)
    {
        $new = new static(
            $data["name"]
        );

        return $new->withDate($data["date"]);
    }
}
final class OutletStocked extends Event
{
    // ...snip

    public static function from(PDO $connection, array $data)
    {
        $new = new static(
            outletNameFromId($connection, $data["outlet"]),
            $data["servings"],
            productNameFromId($connection, $data["product"])
        );

        return $new->withDate($data["date"]);
    }
}

This is from events.php

这是来自events.php

We’re also using a couple of new global functions:

我们还使用了两个新的全局函数:

function productNameFromId(PDO $connection, int $id): string {
    $row = row(
        $connection,
        "SELECT * FROM event_product_invented WHERE id = :id",
        ["id" => $id]
    );

    if (!$row) {
        throw new InvalidArgumentException("Product not found");
    }

    return $row["name"];
}

function outletNameFromId(PDO $connection, int $id): string {
    $row = row(
        $connection,
        "SELECT * FROM event_outlet_opened WHERE id = :id",
        ["id" => $id]
    );

    if (!$row) {
        throw new InvalidArgumentException("Outlet not found");
    }

    return $row["name"];
}

This is from projecting-events.php

这是来自projecting-events.php

The reason we need any of these *NameFromId and *IdFromName functions is because we want to create and present the events using entity names, but we want to store them as foreign keys in the database. That’s just a personal preference of mine, and you’re free to define/present/store them however makes sense to you.

之所以需要这些*NameFromId*IdFromName函数,是因为我们想使用实体名称来创建和呈现事件,但是我们希望将它们作为外键存储在数据库中。 那只是我的个人喜好,您可以自由定义/呈现/存储它们,但是对您来说很有意义。

We can now turn a list of events into database rows, and back again:

现在,我们可以将事件列表转换为数据库行,然后再次返回:

$events = [];

$events[] = new ProductInvented("Chocolate");
$events[] = new ProductPriced("Chocolate", 499);
$events[] = new OutletOpened("Pismo Beach");
$events[] = new OutletStocked("Pismo Beach", 24, "Chocolate");

store($connection, $events); // ← events stored in database
$stored = fetch($connection); // ← events loaded from database

assert(json_encode($events) === json_encode($stored));

Now, how do we convert this to something usable? We need to define a few more helper functions:

现在,我们如何将其转换为可用的东西? 我们需要定义更多辅助函数:

function project(PDO $connection, array $events): array {
    $entities = [
        "products" => [],
        "outlets" => [],
    ];

    foreach ($events as $event) {
        $entities = projectOne($connection, $entities, $event);
    }

    return $entities;
}

function projectOne(PDO $connection, array $entities, ↩
    Event $event): array
{
    if ($event instanceof ProductInvented) {
        $entities = projectProductInvented(
            $connection, $entities, $event
        );
    }

    if ($event instanceof ProductPriced) {
        $entities = projectProductPriced(
            $connection, $entities, $event
        );
    }

    if ($event instanceof OutletOpened) {
        $entities = projectOutletOpened(
            $connection, $entities, $event
        );
    }

    if ($event instanceof OutletStocked) {
        $entities = projectOutletStocked(
            $connection, $entities, $event
        );
    }

    return $entities;
}

This is from projecting-events.php

这是来自projecting-events.php

This code is similar to the code we use to store events. For each type of event, we modify the array of entities. After all the events have been projected, we should have the latest state. Here’s what the other projector methods look like:

此代码类似于我们用于存储事件的代码。 对于每种类型的事件,我们都会修改实体的数组。 预测所有事件之后,我们应该具有最新状态。 其他投影仪方法如下所示:

function projectProductInvented(PDO $connection, ↩
    array $entities, ProductInvented $event): array
{
    $payload = $event->payload();

    $entities["products"][] = [
        "id" => productIdFromName($connection, $payload["name"]),
        "name" => $payload["name"],
    ];

    return $entities;
}

function projectProductPriced(PDO $connection, ↩
    array $entities, ProductPriced $event): array
{
    $payload = $event->payload();

    foreach ($entities["products"] as $i => $product) {
        if ($product["name"] === $payload["product"]) {
            $entities["products"][$i]["price"] = ↩
                $payload["cents"];
        }
    }

    return $entities;
}

function projectOutletOpened(PDO $connection, ↩
    array $entities, OutletOpened $event): array
{
    $payload = $event->payload();

    $entities["outlets"][] = [
        "id" => outletIdFromName($connection, $payload["name"]),
        "name" => $payload["name"],
        "stock" => [],
    ];

    return $entities;
}

function projectOutletStocked(PDO $connection, ↩
    array $entities, OutletStocked $event): array
{
    $payload = $event->payload();

    foreach ($entities["outlets"] as $i => $outlet) {
        if ($outlet["name"] === $payload["outlet"]) {
            foreach ($entities["products"] as $j => $product) {
                if ($product["name"] === $payload["product"]) {
                    $entities["outlets"][$i]["stock"][] = [
                        "product" => &$product,
                        "servings" => $payload["servings"],
                    ];
                }
            }
        }
    }

    return $entities;
}

This is from projecting-events.php

这是来自projecting-events.php

Each of these projection methods accepts a type of event, and sorts through the event payload to make their mark on the $entities array.

这些投影方法中的每一个都接受一种事件,然后对事件有效负载进行排序,以在$entities数组上进行标记。

Projecting events

At this point, we can use the structure we’ve created to populate a website. Since our projectors accept events, we can even generate an initial projection (at the beginning of a request) and then apply any new events to them, as they happen.

在这一点上,我们可以使用我们创建的结构来填充网站。 由于我们的放映机接受事件,因此我们甚至可以生成一个初始投影(在请求开始时),然后在事件发生时对其应用新事件。

As you can probably guess, this isn’t the most efficient way to query a database, just to render a website. If you need the projections a lot of the time, it might be better to periodically project your events and then store the resulting structure in denormalized database tables.

您可能会猜到,这并不是查询数据库的最有效方法,而只是呈现一个网站。 如果您经常需要投影,最好定期投影事件,然后将结果结构存储在非规范化数据库表中。

That way, you can capture events (through things like API requests or form posts), and still query “normal” database tables, when displaying data in an application.

这样,当在应用程序中显示数据时,您可以捕获事件(通过API请求或表单发布之类的事件),并仍然查询“正常”数据库表。

具体投影 (Projecting Specifically)

So far, we’ve seen how we can describe, store, and project (to the latest state). Projecting to a specific point in time is just a matter of adjusting the projection functions, so that they apply events up to, or after, a certain timestamp.

到目前为止,我们已经了解了如何描述,存储和投影(到最新状态)。 投影到特定的时间点只是调整投影功能的问题,以便它们在特定时间戳之前或之后应用事件。

We’ve covered way more than I originally planned to, so I’ll leave that last bit as an exercise for you. Think of how you could model the traditional (for CMS applications) model of draft/working and published versions of content.

我们已经涵盖了比我最初计划的更多的方式,所以我将最后一点留给您练习。 想一想如何为内容的草稿/工作和发布版本建模(对于CMS应用程序)传统模型。

摘要 (Summary)

If you’ve managed to get this far; well done! It’s been a long journey, but worth it I feel. Let us know what you like or don’t like about this design pattern. If you want to learn more about Event Sourcing (or DDD in general), definitely check out the books linked at the start.

如果您设法做到了这一点; 做得好! 这是一段漫长的旅程,但我觉得值得。 让我们知道您喜欢或不喜欢这种设计模式。 如果您想了解有关事件采购(或通常称为DDD)的更多信息,请务必首先查看链接的书籍。

Code full of arrays can get ugly, fast! I highly recommend you check out Adam Wathan’s book, about refactoring loop code to collections.

充满数组的代码会变得丑陋,快速! 我强烈建议您阅读Adam Wathan的书,该书关于将循环代码重构为collection

翻译自: https://www.sitepoint.com/event-sourcing-in-a-pinch/

sap采购组织和采购组

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值