写点什么

如何设计一款大型 Laravel 应用程序的架构(下)?

  • 2020 年 6 月 02 日
  • 本文字数:5156 字

    阅读完需:约 17 分钟

如何设计一款大型Laravel应用程序的架构(下)?


第一篇——《如何设计一款大型 Laravel 应用程序的架构(上)?


本文最初发布于 Freek.dev 博客,经原作者授权由 InfoQ 中文站翻译并分享。


我与同事Brent一起,负责设计一款大型 Laravel 应用程序的架构。我们将这款应用程序分为传统部分和事件溯源部分。在这篇文章中,我想举一个实际例子来说明我们如何实现这一目标。


这是本系列文章中的第二篇。阅读本文前,请先阅读第一部分。此外,你还应该对事件溯源(event sourcing)、聚合和 projector 的作用有很好的理解。这篇介绍可以帮助你快速上手。


设置阶段

首先,我们快速回顾一下 Brent 的文章。我们的应用程序需要事件溯源。在应用的某些部分,我们需要根据过去的信息做出决策;并且随着时间的推移,我们需要能够生成报告,而无需事先了解这些报告中应该有什么内容。


这些问题可以通过事件溯源来解决。我们只存储发生在应用程序中的所有事件,因此我们可以使用聚合(aggregate)根,这些根可以根据事件中的过去信息来做出决策。我们可以创建 projector 并向其回放所有记录的事件,从而生成新报告。


一位名叫 Frank de Jonge 的聪明人曾说过:“事件溯源会让困难的部分变得简单,而让简单的部分变困难”,这说得很对。设置事件溯源需要花费一些时间,而且没那么容易。当你想做一些简单的事情时,还会遇到一些额外的开销。


在我们的应用程序中,我们还有一些相当粗糙的部分,它们不需要事件溯源的能力。在单个应用程序中同时获得两种方法的好处不是很好吗?我和 Brent 研究了这个主题,并结对编程做了一些实验。我们想分享其中一次实验的情况。


一个实际的例子

我们将实现 Brent 文章中提到的两个部分:产品和订单。Product 是很无聊的部分。我们在这里不需要历史记录或报告。对于 Order 部分,我们将使用事件溯源。


你可以在 GitHub 的这个仓库中找到示例的源代码。请记住,这是一个非常简单的示例。在实际应用中所涉及的逻辑可能会更复杂。在这个实验中,我们只是专注研究这样的部分应该/将要如何相互通信。


非事件溯源的部分

查看 Context 目录,你会看到两个部分:Order 和 Product。我们先看一下“Product”:



如你所见,这部分非常简单:这里只有模型和事件。这里的想法是,我们可以像以往那样构建它,不会有太多开销。我们唯一要做的就是在发生某些更改时触发特定事件,以便应用的其他部分能参与其中。


在 Product 模型中,你将看到以下代码


protected $dispatchesEvents = ['created' => ProductCreatedEvent::class,'updated' => ProductUpdatedEvent::class,'deleting' => DeletingProductEvent::class,'deleted' => ProductDeletedEvent::class,];
复制代码


$dispatchesEvents中,你可以定义当模型发生某些事情时应触发的事件。这是标准的 Laravel 功能。当然,我们可以使用原生触发的 Eloquent 事件,但这里我们想要使用自己的事件类,这样才能明确说明问题。


让我们看一下其中一个事件


namespace App\Context\Product\Events;
use App\Context\Product\Models\Product;
class ProductCreatedEvent{ public Product $product;
public function __construct(Product $product) { $this->product = $product; }}
复制代码


如你所见,这里没有什么特别的事情。我们只是将模型用作事件属性。


事件溯源部分

现在,我们转到应用的 Order 部分,该部分是事件溯源的。它的结构如下所示:



你会在此处看到 projector 和聚合根,因此很明显,这部分使用了事件溯源。我们正在使用我们自己的事件溯源包来处理有关存储事件、projector、聚合根……的逻辑。


在应用的 Product 部分,我们触发了事件。这些事件只是常规的 Laravel 事件,我们没有存储它们。ProductEventSubscriber正在侦听这些事件。看一下代码。


namespace App\Context\Order\Subscribers;
use App\Context\Product\Events\DeletingProductEvent as AdminDeletingProductEvent;use App\Context\Product\Events\ProductCreatedEvent as AdminProductCreatedEvent;use App\Context\Order\Events\ProductCreatedEvent;use App\Context\Order\Events\ProductDeletedEvent;use App\Support\Events\EventSubscriber as BaseEventSubscriber;use App\Support\Events\SubscribesToEvents;
class ProductEventSubscriber implements BaseEventSubscriber{ use SubscribesToEvents;
protected array $handlesEvents = [ AdminProductCreatedEvent::class => 'onProductCreated', AdminDeletingProductEvent::class => 'onDeletingProduct', ];
public function onProductCreated(AdminProductCreatedEvent $event): void { $event = $event->product;
event(new ProductCreatedEvent( $event->getUuid(), $event->stock, )); }
public function onDeletingProduct(AdminDeletingProductEvent $event): void { $event = $event->product;
event(new ProductDeletedEvent( $event->getUuid(), )); }}
复制代码


这里发生的事情很有趣。我们将监听来自非事件溯源部分的事件。当来自 Product 上下文的事件(对于 Order 上下文感兴趣)进入时,我们将触发另一个事件。另一个事件是 Product 上下文的一部分。


因此,当出现App\Context\Product\Events\ProductCreatedEvent(且别名为AdminProductCreatedEvent,因为可能是管理员在 UI 上执行的操作导致该操作)时,我们将触发App\Context\Order\Events\ProductCreatedEvent(来自 Order 上下文)。


我们将获取对 Order 上下文感兴趣的所有属性,并将它们放在App\Context\Order\Events\ProductCreatedEvent中。


public function onProductCreated(AdminProductCreatedEvent $event): void{    $event = $event->product;
event(new ProductCreatedEvent( $event->getUuid(), $event->stock, ));}
复制代码


看一下这个事件本身。


namespace App\Context\Order\Events;
use App\Support\ValueObjects\ProductUuid;use Spatie\EventSourcing\ShouldBeStored;
class ProductCreatedEvent extends ShouldBeStored{ public ProductUuid $productUuid;
public int $stock;
public function __construct(ProductUuid $productUuid, int $stock) { $this->productUuid = $productUuid;
$this->stock = $stock; }}
复制代码


你可以看到这个事件扩展了ShouldBeStored。这个基类是我们的事件溯源包的一部分。这将导致该事件被存储下来。


我们立即使用这个存储的事件来建立一个包含存货(Stock)的 projection。看一下ProductStockProjector


namespace App\Context\Order\Projectors;
use App\Context\Order\Events\OrderCancelledEvent;use App\Context\Order\Events\ProductCreatedEvent;use App\Context\Order\Events\ProductDeletedEvent;use App\Context\Order\Events\OrderCreatedEvent;use App\Context\Order\Models\Order;use App\Context\Order\Models\ProductStock;use App\Support\ValueObjects\ProductStockUuid;use Spatie\EventSourcing\Projectors\Projector;use Spatie\EventSourcing\Projectors\ProjectsEvents;
class ProductStockProjector implements Projector{ use ProjectsEvents;
protected array $handlesEvents = [ ProductCreatedEvent::class => 'onProductCreated', ProductDeletedEvent::class => 'onProductDeleted', OrderCreatedEvent::class => 'onOrderCreated', OrderCancelledEvent::class => 'onOrderCancelled', ];
public function onProductCreated(ProductCreatedEvent $event): void { ProductStock::create([ 'uuid' => ProductStockUuid::create(), 'product_uuid' => $event->productUuid, 'stock' => $event->stock, ]); }
public function onProductDeleted(ProductDeletedEvent $event): void { ProductStock::forProduct($event->productUuid)->delete(); }
public function onOrderCreated(OrderCreatedEvent $event): void { $productStock = ProductStock::forProduct($event->productUuid);
$productStock->update([ 'stock' => $productStock->stock - $event->quantity, ]); }
public function onOrderCancelled(OrderCancelledEvent $event): void { $order = Order::findByUuid($event->aggregateRootUuid());
$productStock = ProductStock::forProduct($order->product->uuid);
$productStock->update([ 'stock' => $productStock->stock + $order->quantity, ]); }}
复制代码


触发ProductCreatedEvent时,我们将调用ProductStock::createProductStock是常规的 Eloquent 模型。


namespace App\Context\Order\Models;
use App\Context\Product\Models\Product;use Illuminate\Database\Eloquent\Model;
class ProductStock extends Model{ /** * @param \App\Context\Product\Models\Product|\App\Support\ValueObjects\ProductUuid $productUuid * * @return \App\Context\Order\Models\ProductStock */ public static function forProduct($productUuid): ProductStock { if ($productUuid instanceof Product) { $productUuid = $productUuid->getUuid(); }
return static::query() ->where('product_uuid', $productUuid) ->first(); }}
复制代码


ProductStockProjector中,当收到订单、取消订单或删除订单时,将更新ProductStock模型。


现在,我们与参与该项目的每位成员都达成一项协议,那就是不允许从其他上下文写入模型。Order 上下文可以从这个模型写入和读取,而 Product 上下文只能读取它。由于这是一个重要的规则,因此我们可能会很快用代码把它固定下来。


最后,我们看一下使用ProductStock模型进行决策的OrderAggregateRoot


namespace App\Context\Order;
use App\Context\Product\Models\Product;use App\Context\Order\Events\CouldNotCreateOrderBecauseInsufficientStock;use App\Context\Order\Events\OrderCreatedEvent;use App\Context\Order\Events\OrderCancelledEvent;use App\Context\Order\Exceptions\CannotCreateOrderBecauseInsufficientStock;use App\Context\Order\Models\ProductStock;use Spatie\EventSourcing\AggregateRoot;
class OrderAggregateRoot extends AggregateRoot{ public function createOrder(Product $product, int $quantity): self { $eventAvailability = ProductStock::forProduct($product);
if ($eventAvailability->availability < $quantity) { $this->recordThat(new CouldNotCreateOrderBecauseInsufficientStock( $product->getUuid(), $quantity ));
throw CannotCreateOrderBecauseInsufficientStock::make(); }
$unitPrice = $product->unit_price;
$totalPrice = $unitPrice * $quantity;
$this->recordThat(new OrderCreatedEvent( $product->getUuid(), $quantity, $unitPrice, $totalPrice, ));
return $this; }
public function cancelOrder(): self { $this->recordThat(new OrderCancelledEvent());
return $this; }}
复制代码


在一个完整的应用中,createOrder函数可能由 UI 中执行的操作触发。产品可以被订购一定的数量。使用ProductStock,聚合根将决定是否有足够的库存来创建订单。


小结

应用的非事件溯源部分会触发常规事件。事件溯源部分会监听这些事件,并在它们自己的事件中记录它们感兴趣的数据。这些属于上下文的事件将被存储下来。这些存储的事件将提供给 projector。这些 projector 创建的模型可以由非事件溯源的部分读取。非事件溯源的部分只能读取这些模型,不能写入。聚合根可以使用事件和 projection 来做出决策。


这种方法可以带来很多好处:


  • 可以像我们过去那样构建非事件溯源的部分。我们只需要注意被触发的事件即可

  • 事件溯源部分可以记录自己的事件以采取行动

  • 我们可以创建新的 projector 并重播所有记录的事件,以建立新状态


我知道这些要吸收起来没那么容易。这种方法并不是一成不变的。我们尚处于探索阶段,但我们对于已经掌握的部分信心十足。


英文原文:


Mixing event sourcing in a traditional Laravel app


2020 年 6 月 02 日 16:461185
用户头像
王强 技术是文明进步的力量

发布了 719 篇内容, 共 274.4 次阅读, 收获喜欢 1569 次。

关注

评论

发布
暂无评论
发现更多内容

使用xdebug调试php详细教程

CRMEB

华为云持续快速增长,IaaS市场排名中国第二、全球第五

科技热闻

2022年国内外主流的10款Bug跟踪管理软件

PingCode

netty系列之:可能有人听过ThreadLocal,但一定没人听过ThreadLocal对象池

程序那些事

Java Netty 程序那些事 4月月更

云电脑的四重守护,安全有谱

天翼云开发者社区

安全 云电脑

【三级等保】三级等保办理流程经验大分享

行云管家

网络安全 等保 等级保护 等保三级 等保2.0

国产ETL自动化运维调度工具 TASKCTL 控制容器概述

TASKCTL

大数据 DevOps 分布式 自动化运维 Kafka ETL

低通信量是筛选分布式多方安全计算常用安全协议方案的先决条件

易观分析

安全多方计算 低通信量

蒙牛乳业加入星策开源社区,携手推动企业智能化转型建设

星策开源社区

人工智能 机器学习 企业数智化 智能化转型

web前端培训React 中Router的必备知识点

@零度

前端开发 React

总结JAVA全栈知识点,七面阿里成功斩获P8Offer

Java架构追梦

程序员 java面试 后端开发

Apache APISIX Summit ASIA 2022——5月20日开启开源生态探索之旅

Apache APISIX 中国社区

网关 API网关 APISIX 开源生态 summit

DM 是如何处理 DML 的丨TiDB 工具分享

PingCAP

狩猎者Hunter夹子机器人系统开发(成熟技术)

Weix_StPv888

【架构视角】一篇文章带你彻底吃透Spring

潘潘和他的朋友们

spring 后端 springboot 后端开发

我以为自己MySQL够牛逼了,直到看到了Alibaba的面试题,是我不配了。。

Java架构追梦

Java 后端开发 MySQL 运维 程序员面试

基于SpringBoot2+redis+Vue商管系统,秒杀等,可二次开发接私活

Java架构追梦

Java spring 后端开发

【高并发】如何使用互斥锁解决多线程的原子性问题?这次终于明白了!

冰河

并发编程 多线程 协程 异步编程 精通高并发系列

大数据培训Spark SQL知识点与实战分析

@零度

spark 大数据开发

蚂蚁三面被挂,幸获内推,历经5轮终于拿到口碑offer

Java架构追梦

java面试 后端开发 程序员面试 大厂Offer

划重点,2022 常见的面试题和八股文都为大家总结出来了

Java架构追梦

程序员 java面试 后端开发 Java面试八股文

使用APICloud & MobTech SDK 快速实现分享到社交平台功能

APICloud

APP开发 APICloud MobTech袤博科技 社交分享

HDI硬件设备接口介绍

科技汇

已开源!分发业务看过来【跨端动态模板引擎】

阿里巴巴文娱技术

ios android 开源 开发工具 移动开发

原生JavaScript灵魂拷问(二),你能全部答对吗?

战场小包

JavaScript 前端 4月月更

【堡垒机】2022年网络安全堡垒机厂商排名看这里!

行云管家

网络安全 堡垒机 移动云 网络安全堡垒机

神秘男嘉宾登场,引爆全场灯光?

龙智—DevSecOps解决方案

Atlassian Confluence confluence插件

恒源云(Gpushare)_VSA:一个可变形尺寸窗口自注意力模型

恒源云

深度学习 GPU算力

没想到我也可以入职阿里,两年CRUD,二本毕业,备战两个月面试阿里,侥幸拿下offer定

Java架构追梦

Java 程序员 后端开发

如何设计一款大型Laravel应用程序的架构(下)?_文化 & 方法_Freek_InfoQ精选文章