• Час читання ~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, щоб передати те, що вам потрібно, у метод робочих місць 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? Що ви зробили? Повідомте нас у Твіттері!

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