在 Laravel 中建模业务流程

发布时间 作者

Modelling Busines Processes in Laravel image

作为开发者,我们经常将业务流程映射到数字流程,从发送电子邮件到更复杂的事情。让我们看看如何将更复杂的流程写入干净且优雅的代码。

一切从工作流开始。我在推特上发布了关于撰写本教程的信息,以了解人们对业务流程是否有任何反馈 - 不过,我只收到了一条回复。

因此,考虑到这一点,让我们看看订单/运输流程,这是一个有足够多活动部件的流程,可以帮助我们理解这个概念 - 但我不会从领域逻辑的角度进行过多详细说明。

想象一下,您经营一家在线商品商店,有一个在线商店,并使用一件代发服务来根据需要在客户下单时发送商品。我们需要考虑在没有任何数字化帮助的情况下业务流程可能是什么样的 - 这使我们能够理解业务及其需求。

请求一件商品(我们使用按需打印服务,因此库存不是问题)。我们获取客户的详细信息。我们为这位新客户创建一个订单。我们接受此订单的付款。我们向客户确认订单和付款。然后,我们将订单提交到按需打印服务。

按需打印服务将定期向我们更新订单状态,我们可以通过该状态更新客户,但这将是一个不同的业务流程。首先,让我们看看订单流程,并想象一下,所有这些都是在单个控制器中完成的。这将变得非常复杂,难以管理或更改。

class PlaceOrderController
{
public function __invoke(PlaceOrderRequest $request): RedirectResponse
{
// Create our customer record.
$customer = Customer::query()->create([]);
 
// Create an order for our customer.
$order = $customer->orders()->create([]);
 
try {
// Use a payment library to take payment.
$payment = Stripe::charge($customer)->for($order);
} catch (Throwable $exception) {
// Handle the exception to let the customer know payment failed.
}
 
// Confirm the order and payment with the customer.
Mail::to($customer->email)->send(new OrderProcessed($customer, $order, $payment));
 
// Send the order to the Print-On-Demand service
MerchStore::create($order)->for($customer);
 
Session::put('status', 'Your order has been placed.');
 
return redirect()->back();
}
}

因此,如果我们逐步执行此代码,我们会发现,我们创建了一个用户和订单 - 然后接受付款并发送电子邮件。最后,我们在会话中添加一个状态消息并重定向客户。

因此,我们两次写入数据库,与支付 API 交互,发送电子邮件,最后写入会话并重定向。对于单个同步线程来说,要处理很多事情,而且有很多事情可能会出错。这里的逻辑步骤是将此移到后台作业,以便我们获得一定程度的容错能力。

class PlaceOrderController
{
public function __invoke(PlaceOrderRequest $request): RedirectResponse
{
// Create our customer record.
$customer = Customer::query()->create([]);
 
dispatch(new PlaceOrder($customer, $request));
 
Session::put('status', 'Your order is being processed.');
 
return redirect()->back();
}
}

我们已经清理了很多控制器 - 但是,我们只是将问题移到了后台进程。虽然将此移到后台进程是处理此问题的正确方法,但我们需要以完全不同的方式来处理它。

首先,我们想要先创建或获取客户 - 以防他们之前已经下单。

class PlaceOrderController
{
public function __invoke(PlaceOrderRequest $request): RedirectResponse
{
// Create our customer record.
$customer = Customer::query()->firstOrCreate([], []);
 
dispatch(new PlaceOrder($customer, $request));
 
Session::put('status', 'Your order is being processed.');
 
return redirect()->back();
}
}

我们的下一步是将客户创建操作移到一个共享类中 - 这是我们想要创建或获取客户记录的多种情况之一。

class PlaceOrderController
{
public function __construct(
private readonly FirstOrCreateCustomer $action,
) {}
 
public function __invoke(PlaceOrderRequest $request): RedirectResponse
{
// Create our customer record.
$customer = $this->action->handle([]);
 
dispatch(new PlaceOrder($customer, $request));
 
Session::put('status', 'Your order is being processed.');
 
return redirect()->back();
}
}

让我们看看如果将代码直接移到后台进程,那么后台进程代码会是什么样的。

class PlaceOrder implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
 
public function _construct(
public readonly Customer $customer,
public readonly Request $request,
) {}
 
public function handle(): void
{
// Create an order for our customer.
$order = $this->customer->orders()->create([]);
 
try {
// Use a payment library to take payment.
$payment = Stripe::charge($this->customer)->for($order);
} catch (Throwable $exception) {
// Handle the exception to let the customer know payment failed.
}
 
// Confirm the order and payment with the customer.
Mail::to($this->customer->email)
->send(new OrderProcessed($this->customer, $order, $payment));
 
// Send the order to the Print-On-Demand service
MerchStore::create($order)->for($this->customer);
}
}

还不错,但是 - 如果某个步骤失败了,我们重新尝试作业怎么办?我们会一遍又一遍地重复执行某些过程,而这些过程是不需要的。我们首先应该考虑在数据库事务中创建订单。

class CreateOrderForCustomer
{
public function handle(Customer $customer, data $payload): Model
{
return DB::transaction(
callback: static fn () => $customer->orders()->create(
attributes: $payload,
),
);
}
}

现在,我们可以更新后台进程来实现这个新命令。

class PlaceOrder implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
 
public function _construct(
public readonly Customer $customer,
public readonly Request $request,
) {}
 
public function handle(CreateOrderForCustomer $command): void
{
// Create an order for our customer.
$order = $command->handle(
customer: $customer,
payload: $this->request->only([]),
);
 
try {
// Use a payment library to take payment.
$payment = Stripe::charge($this->customer)->for($order);
} catch (Throwable $exception) {
// Handle the exception to let the customer know payment failed.
}
 
// Confirm the order and payment with the customer.
Mail::to($this->customer->email)
->send(new OrderProcessed($this->customer, $order, $payment));
 
// Send the order to the Print-On-Demand service
MerchStore::create($order)->for($this->customer);
}
}

这种方法效果很好。但是,它不是理想的,而且您在任何时候都没有太多可见性。我们可以以不同的方式对其进行建模,以便我们对业务流程进行建模,而不是将其分解为多个部分。

一切从 Pipeline facade 开始,它使我们能够正确构建此流程。我们仍然希望在控制器中创建客户,但是我们将在后台作业中使用业务流程处理其余流程。

首先,我们需要一个抽象类,我们的业务流程类可以扩展该抽象类,以最大限度地减少代码重复。

abstract class AbstractProcess
{
public array $tasks;
 
public function handle(object $payload): mixed
{
return Pipeline::send(
passable: $payload,
)->through(
pipes: $this->tasks,
)->thenReturn();
}
}

我们的业务流程类将拥有许多关联的任务,我们将在实现中声明这些任务。然后,我们的抽象流程将获取传递的有效负载并将其发送到这些任务中 - 最终返回。不幸的是,我想不出一个很好的方法来返回实际类型而不是混合类型,但有时我们必须妥协...

class PlaceNewOrderForCustomer extends AbstractProcess
{
public array $tasks = [
CreateNewOrderRecord::class,
ChargeCustomerForOrder::class,
SendConfirmationEmail::class,
SendOrderToStore::class,
];
}

如您所见,这看起来非常干净,而且效果很好。这些任务可以在其他业务流程中重复使用,只要这些任务有意义。

class PlaceOrder implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
 
public function _construct(
public readonly Customer $customer,
public readonly Request $request,
) {}
 
public function handle(PlaceNewOrderForCustomer $process): void
{
try {
$process->handle(
payload: new NewOrderForCustomer(
customer: $this->customer->getKey(),
orderPayload: $this->request->only([]),
),
);
} catch (Throwable $exception) {
// Handle the potential exceptions that could occur.
}
}
}

我们的后台进程现在尝试处理业务流程,如果发生任何异常,我们可以失败并稍后重新尝试该流程。由于 Laravel 将使用它的 DI 容器将您需要的内容传递到作业的 handle 方法中,因此我们可以将我们的流程类传递到此方法中,让 Laravel 为我们解析它。

class CreateNewOrderRecord
{
public function __invoke(object $payload, Closure $next): mixed
{
$payload->order = DB::transaction(
callable: static fn () => Order::query()->create(
attributes: [
$payload->orderPayload,
'customer_id' $payload->customer,
],
),
);
 
return $next($payload);
}
}

我们的业务流程任务是可调用类,这些类获取“旅客”,即我们要传递的有效负载,以及一个闭包,即管道中的下一个任务。这类似于 Laravel 中中间件的功能,我们可以根据需要链接任意数量的中间件,它们只是按顺序调用。

我们传递的有效负载可以是一个简单的 PHP 对象,我们可以使用它来构建对象,因为它遍历管道,在每个步骤中扩展它,从而允许管道中的下一个任务访问它需要的任何信息,而无需运行数据库查询。

使用这种方法,我们可以分解那些不是数字化的业务流程,并为它们创建数字表示。通过这种方式将它们链接起来,可以在需要的地方添加自动化。这确实是一种非常简单的方法,但它非常强大。

您在 Laravel 中找到了处理业务流程的好方法吗?您做了什么?请在推特上告诉我们!

Steve McDougall photo

Laravel 新闻 的技术作家,Treblle 的开发者倡导者。API 专家,经验丰富的 PHP/Laravel 工程师。 YouTube 直播主

归档于
Cube

Laravel 新闻稿

加入 4 万多名其他开发者,不要错过新的技巧、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI 进行 Tinker,并获得本地和生产环境的自动完成和即时反馈。

Tinkerwell
No Compromises logo

不妥协

来自 No Compromises 播客的两位经验丰富的开发者 Joel 和 Aaron 现在可以为您的 Laravel 项目提供服务。 ⬧ 固定费率为 7500 美元/月。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 返还保证。

不妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保您的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,让您的应用程序保持新鲜感。

Shift
Bacancy logo

Bacancy

只需每月$2500,即可获得拥有4-6年经验的资深 Laravel 开发人员,为您的项目注入活力。获得 160 小时的专业知识,并享受 15 天的无风险试用。立即安排通话!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - Laravel 开发的理想选择,拥有超过十年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 电子商务。一个开源软件包,为 Laravel 带来了现代无头电子商务功能的强大功能。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 招聘平台

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS 启动套件

SaaSykit 是一个 Laravel SaaS 启动套件,它包含运行现代 SaaS 所需的所有功能。支付、精美结账、管理面板、用户仪表板、身份验证、即用型组件、统计、博客、文档等。

SaaSykit: Laravel SaaS 启动套件
Rector logo

Rector

您的无缝 Laravel 升级合作伙伴,降低成本,加速创新,助力企业成功

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成,增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。探索 MongoDB 灵活、现代的数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel Prompts 构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 软件包将评论添加到您的 Laravel 应用程序

阅读文章