在Laravel中构建业务流程模型
作为开发者,我们经常将业务流程映射为数字流程,从发送电子邮件到相当复杂的东西。
让我们来看看如何把一个更复杂的流程,写成干净而优雅的代码。
这一切都从工作流程开始。
我在推特上写了这个教程,看看是否会有任何关于业务流程的反馈,人们会觉得有帮助--不过我只得到了一个回应。
> 下一个教程决定了! 在Laravel中映射业务流程
密切关注@laravelnews的报道
如果你有一个你想看到的业务流程映射的例子,请留言! #php #phpc #laravel
- JustSteveKing (@JustSteveKing) 3月22日, 2023
因此,考虑到这一点,让我们来看看订单/运输过程,这个过程有足够多的活动部件来表达这个想法
--但我不会从领域逻辑的角度去讨论太多细节。
想象一下,你经营一家在线商品商店,有一个在线商店,并使用滴滴出行服务,在下订单时按需发送商品。
我们需要考虑在没有任何数字帮助的情况下,业务流程可能是什么样的
--这使我们能够理解业务和它的需求。
要求提供商品(我们使用的是按需印刷服务,所以库存不是问题)。
我们获取客户的详细资料。
我们为这个新客户创建一个订单。
我们接受这个订单的付款。
我们向客户确认订单和付款。
然后我们向按需印刷服务下订单。
按需印刷服务将定期更新我们的订单状态,我们可以更新我们的客户,但这将是一个不同的业务流程。
让我们先看一下订单流程,想象一下这一切都在一个控制器中在线完成。
这将使管理或改变变得相当复杂。
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);
}
}
这种方法效果不错。然而,这并不理想,你在任何时候都没有多少可见性。
我们可以用不同的方式来建模,这样我们就可以对我们的业务流程进行建模,而不是把它分割成几个部分。
这一切都从管道门面开始,使我们能够正确地建立这个流程。
我们仍然希望在控制器中创建我们的客户,但我们将在后台工作中使用业务流程处理其余的过程。
首先,我们需要一个抽象类,我们的业务流程类可以扩展,以减少代码的重复。
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容器来传递你所需要的东西到jobs 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);
}
}
我们的业务流程任务是可调用的类, 它被传递给 "旅行者", 也就是我们要传递的有效载荷, 和一个Closure, 也就是管道中的下一个任务。
这类似于Laravel中的中间件功能的工作方式, 我们可以根据自己的需要进行链式调用, 而他们只是按顺序调用.
我们传入的有效载荷可以是一个简单的PHP对象,我们可以用它来构建,
因为它通过管道,在每一步扩展它,允许管道中的下一个任务访问任何它需要的信息,而无需运行数据库查询。
使用这种方法,我们可以将我们的业务流程分解为非数字化的流程,并对其进行数字化表示。
以这种方式将它们串联起来,在我们需要的地方增加自动化。
这是一个相当简单的方法,真的,但它是非常强大的。
你有没有在Laravel中找到一个处理业务流程的好方法?
你是怎么做的? 请在twitter上告诉我们!
转:
https://laravel-news.com/modelling-busines-processes-in-laravel
相关文章