在 Laravel 中建模业务流程
发布时间 作者 史蒂夫·麦克杜格尔
作为开发者,我们经常将业务流程映射到数字流程,从发送电子邮件到更复杂的事情。让我们看看如何将更复杂的流程写入干净且优雅的代码。
一切从工作流开始。我在推特上发布了关于撰写本教程的信息,以了解人们对业务流程是否有任何反馈 - 不过,我只收到了一条回复。
下一个教程决定了!在 Laravel 中映射业务流程 👀
— JustSteveKing (@JustSteveKing) 2023 年 3 月 22 日
关注 @laravelnews,了解最新消息 🔥🔥
如果您想看到某个示例业务流程的映射,请留言!#php #phpc #laravel
因此,考虑到这一点,让我们看看订单/运输流程,这是一个有足够多活动部件的流程,可以帮助我们理解这个概念 - 但我不会从领域逻辑的角度进行过多详细说明。
想象一下,您经营一家在线商品商店,有一个在线商店,并使用一件代发服务来根据需要在客户下单时发送商品。我们需要考虑在没有任何数字化帮助的情况下业务流程可能是什么样的 - 这使我们能够理解业务及其需求。
请求一件商品(我们使用按需打印服务,因此库存不是问题)。我们获取客户的详细信息。我们为这位新客户创建一个订单。我们接受此订单的付款。我们向客户确认订单和付款。然后,我们将订单提交到按需打印服务。
按需打印服务将定期向我们更新订单状态,我们可以通过该状态更新客户,但这将是一个不同的业务流程。首先,让我们看看订单流程,并想象一下,所有这些都是在单个控制器中完成的。这将变得非常复杂,难以管理或更改。
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 中找到了处理业务流程的好方法吗?您做了什么?请在推特上告诉我们!