在Laravel中构建业务流程模型

2023-06-01 00:00:00 模型 业务流程 构建

作为开发者,我们经常将业务流程映射为数字流程,从发送电子邮件到相当复杂的东西。

让我们来看看如何把一个更复杂的流程,写成干净而优雅的代码。

这一切都从工作流程开始。

我在推特上写了这个教程,看看是否会有任何关于业务流程的反馈,人们会觉得有帮助--不过我只得到了一个回应。

> 下一个教程决定了! 在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

相关文章