• Время чтения ~7 мин
  • 24.03.2023

Как разработчики, мы часто сопоставляем бизнес-процессы с цифровыми процессами, от отправки электронной почты до чего-то довольно сложного. Давайте рассмотрим, как взять более сложный процесс и написать чистый и элегантный код.

Все начинается с рабочего процесса. Я написал в Твиттере о написании этого учебника, чтобы посмотреть, будет ли какая-либо обратная связь о бизнес-процессах, которую люди сочтут полезной - хотя я действительно получил только один ответ.

Следующий туториал решил! Картографирование бизнес-процессов в Laravel 👀

Следите за @laravelnews для этого 🔥🔥

Если у вас есть пример бизнес-процесса, который вы хотели бы видеть на карте, оставьте комментарий! #php #phpc #laravel

— JustSteveKing (@JustSteveKing) March 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? Что Вы сделали? Дайте нам знать в твиттере!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297