• Czas czytania ~7 min
  • 24.03.2023

Jako programiści często mapujemy procesy biznesowe na procesy cyfrowe, od wysłania wiadomości e-mail po coś dość złożonego. Przyjrzyjmy się, jak wziąć bardziej skomplikowany proces i napisać czysty i elegancki kod.

Wszystko zaczyna się od przepływu pracy. Napisałem na Twitterze o napisaniu tego samouczka, aby sprawdzić, czy będą jakieś informacje zwrotne na temat procesów biznesowych, które ludzie uznają za pomocne - tak naprawdę dostałem tylko jedną odpowiedź.

Następny samouczek zdecydowany! Mapowanie procesów biznesowych w Laravel 👀

Miej oko na @laravelnews dla tego Jeśli 🔥🔥

masz przykładowy proces biznesowy, który chcesz zobaczyć na mapie, upuść komentarz! #php #phpc #laravel

— JustSteveKing (@JustSteveKing) March 22, 2023

Mając to na uwadze, spójrzmy na proces zamówienia / wysyłki, coś z wystarczającą liczbą ruchomych części, aby przekazać pomysł - ale nie będę wchodził w zbyt wiele szczegółów z perspektywy logiki domeny.

Wyobraź sobie, że prowadzisz sklep internetowy z produktami, masz sklep internetowy i korzystasz z usługi dropshipping, aby wysyłać towary na żądanie po złożeniu zamówienia. Musimy zastanowić się, jak mógłby wyglądać proces biznesowy bez pomocy cyfrowej – to pozwala nam zrozumieć biznes i jego potrzeby.

Żądany jest towar (korzystamy z usługi drukowania na żądanie, więc magazyn nie stanowi problemu).
Przyjmujemy dane klientów.
Tworzymy zamówienie dla tego nowego klienta.
Akceptujemy płatności za to zamówienie.
Potwierdzamy zamówienie i płatność na rzecz klienta.
Następnie składamy zamówienie w usłudze druku na żądanie.

Usługa druku na żądanie będzie okresowo informować nas o statusie zamówienia, który możemy aktualizować naszym klientom, ale byłby to inny proces biznesowy. Spójrzmy najpierw na proces zamawiania i wyobraźmy sobie, że wszystko to zostało wykonane w jednym kontrolerze. Zarządzanie lub zmiana byłyby dość skomplikowane.

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();
    }
}

Jeśli więc przejdziemy przez ten kod, zobaczymy, że tworzymy użytkownika i zamówienie - a następnie akceptujemy płatność i wysyłamy e-mail. Na koniec dodajemy komunikat o stanie do sesji i przekierowujemy klienta.

Piszemy więc do bazy dwa razy, rozmawiamy z API płatności, wysyłamy maila, a na koniec piszemy do sesji i przekierowujemy. Jest to całkiem sporo w jednym synchronicznym wątku do obsługi, z dużym potencjałem do zepsucia. Logicznym krokiem jest przeniesienie tego do zadania w tle, abyśmy mieli poziom odporności na błędy.

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();
    }
}

Dużo oczyściliśmy nasz kontroler - jednak wszystko, co zrobiliśmy, to przeniesienie problemu do procesu w tle. Chociaż przeniesienie tego do procesu w tle jest właściwym sposobem radzenia sobie z tym, musimy podejść do tego zupełnie inaczej.

Po pierwsze, chcemy najpierw stworzyć klienta - na wypadek, gdyby wcześniej złożył zamówienie.

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();
    }
}

Naszym następnym krokiem jest przeniesienie tworzenia klienta do wspólnej klasy - jest to jeden z wielu przypadków, w których chcielibyśmy utworzyć lub uzyskać rekord klienta.

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();
    }
}

Spójrzmy na kod procesu w tle, jeśli przenieśliśmy go bezpośrednio tam.

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);
    }
}

Nieźle, ale - co jeśli krok się nie powiedzie i ponowimy próbę? Skończymy na powtarzaniu części tego procesu raz za razem, gdy nie będzie to potrzebne. Powinniśmy najpierw spojrzeć na utworzenie zamówienia w ramach transakcji bazy danych.

class CreateOrderForCustomer
{
    public function handle(Customer $customer, data $payload): Model
    {
        return DB::transaction(
            callback: static fn () => $customer->orders()->create(
                attributes: $payload,
            ),
        );
    }
}

Teraz możemy zaktualizować nasz proces w tle, aby zaimplementować to nowe polecenie.

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);
    }
}

Takie podejście działa dobrze. Jednak nie jest to idealne rozwiązanie i w żadnym momencie nie masz dużej widoczności. Moglibyśmy modelować to inaczej, abyśmy modelowali nasz proces biznesowy, zamiast dzielić go na części.

Wszystko zaczyna się od fasady rurociągu, co pozwala nam poprawnie zbudować ten proces. Nadal będziemy chcieli utworzyć naszego klienta w kontrolerze, ale resztę procesu zajmiemy się w ramach zadania w tle przy użyciu procesu biznesowego.

Na początek będziemy potrzebować klasy abstrakcyjnej, którą nasze klasy procesów biznesowych mogą rozszerzyć, aby zminimalizować duplikację kodu.

abstract class AbstractProcess
{
    public array $tasks;

    public function handle(object $payload): mixed
    {
        return Pipeline::send(
            passable: $payload,
        )->through(
            pipes: $this->tasks,
        )->thenReturn();
    }
}

Nasza klasa procesów biznesowych będzie miała wiele powiązanych zadań, które deklarujemy w implementacji. Następnie nasz abstrakcyjny proces weźmie przekazany ładunek i wyśle go przez te zadania - ostatecznie zwróci. Niestety, nie mogę wymyślić dobrego sposobu na przywrócenie rzeczywistego typu zamiast mieszanego, ale czasami musimy iść na kompromis ...

class PlaceNewOrderForCustomer extends AbstractProcess
{
    public array $tasks = [
        CreateNewOrderRecord::class,
        ChargeCustomerForOrder::class,
        SendConfirmationEmail::class,
        SendOrderToStore::class,
    ];
}

Jak widać, jest to super czyste i działa dobrze. Zadania te można ponownie wykorzystać w innych procesach biznesowych, w których ma to sens.

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.
        }
    }
}

Nasz proces w tle próbuje teraz obsłużyć proces biznesowy, a jeśli wystąpią jakiekolwiek wyjątki, możemy zakończyć się niepowodzeniem i ponowić próbę później. Ponieważ Laravel użyje swojego kontenera DI, aby przekazać to, czego potrzebujesz do metody zadań handle , możemy przekazać naszą klasę procesu do tej metody i pozwolić Laravel rozwiązać to za nas.

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);
    }
}

Nasze zadania procesów biznesowych to klasy invokable, które przechodzą "podróżnik", czyli ładunek, przez który chcemy przejść, oraz zamknięcie, które jest kolejnym zadaniem w potoku. Jest to podobne do tego, jak działa funkcjonalność oprogramowania pośredniczącego w Laravel, gdzie możemy połączyć tyle, ile potrzebujemy, i są one po prostu sekwencyjnie wywoływane.

Ładunek, który przekazujemy, może być prostym obiektem PHP, którego możemy użyć do zbudowania, gdy przechodzi przez potok, rozszerzając go na każdym kroku, umożliwiając następnemu zadaniu w potoku dostęp do wszelkich potrzebnych informacji bez uruchamiania zapytania do bazy danych.

Korzystając z tego podejścia, możemy rozbić nasze procesy biznesowe, które nie są cyfrowe i stworzyć ich cyfrową reprezentację. Łączenie ich w ten sposób zwiększa automatyzację tam, gdzie jej potrzebujemy. Jest to naprawdę dość proste podejście, ale jest bardzo potężne.

Czy znalazłeś dobry sposób na obsługę procesów biznesowych w Laravel? Co zrobiłeś? Daj nam znać na Twitterze!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

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...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297