Создание бизнес-логики на PHP: от статусов до шины событий

Раздел: Корпоративные решения на PHP -> Автоматизация бизнес-процессов

Основные подходы к реализации бизнес-процессов на PHP

Как реализовать надежное управление статусами заказов с помощью Symfony Workflow?

Symfony Workflow предоставляет мощный механизм для формализации бизнес-процессов в виде конечного автомата или рабочего графа. Основное преимущество - декларативное описание состояний и переходов с возможностью встраивания проверок и действий на каждом этапе.


# config/packages/workflow.yaml
framework:
    workflows:
        order_workflow:
            type: 'state_machine'
            marking_store:
                type: 'method'
                property: 'status'
            supports:
                - App\Entity\Order
            initial_marking: new
            places:
                - new
                - processing
                - shipped
                - completed
                - cancelled
            transitions:
                process:
                    from: new
                    to: processing
                ship:
                    from: processing
                    to: shipped
                complete:
                    from: shipped
                    to: completed
                cancel:
                    from: [new, processing]
                    to: cancelled

бизнес процессы php код (php-код для бизнес-процессов)

В данном примере определен workflow для сущности Order. Каждый переход может быть дополнен событиями и проверками. Использование state_machine гарантирует, что объект находится только в одном состоянии в любой момент времени.


// src/Controller/OrderController.php
use Symfony\Component\Workflow\WorkflowInterface;

public function processOrder(Order $order, WorkflowInterface $orderWorkflow)
{
    if ($orderWorkflow->can($order, 'process')) {
        $orderWorkflow->apply($order, 'process');
        $entityManager->flush();
    }
}

Метод can проверяет возможность перехода, apply выполняет его. Это предотвращает недопустимые изменения статуса.

Типичная ошибка - забыть вызвать flush() после применения перехода, что приводит к несохранению состояния. Решение: всегда синхронизировать изменения с базой данных. Другая распространенная проблема - отсутствие перехода при изменении требования к процессу: необходимо обновлять конфигурацию workflow и миграции для существующих данных.

Цель использования: управление сложными процессами с множеством статусов (заказы, документы, заявки) с гарантией целостности. Подходит для средних и крупных проектов.

Как создать простой конечный автомат без внешних библиотек?

Для небольших проектов или MVP можно реализовать собственный автомат на основе массива правил. Это дает полный контроль и не требует установки дополнительных пакетов.


class SimpleStateMachine
{
    private array $transitions = [];
    private string $currentState;

    public function __construct(string $initialState, array $transitions)
    {
        $this->currentState = $initialState;
        $this->transitions = $transitions;
    }

    public function can(string $transition): bool
    {
        return isset($this->transitions[$transition]) &&
               in_array($this->currentState, $this->transitions[$transition]['from']);
    }

    public function apply(string $transition): void
    {
        if (!$this->can($transition)) {
            throw new \InvalidArgumentException("Transition $transition not allowed from state $this->currentState");
        }
        $this->currentState = $this->transitions[$transition]['to'];
    }

    public function getState(): string
    {
        return $this->currentState;
    }
}

// Использование:
$transitions = [
    'process' => ['from' => ['new'], 'to' => 'processing'],
    'ship' => ['from' => ['processing'], 'to' => 'shipped'],
];
$machine = new SimpleStateMachine('new', $transitions);
echo $machine->getState(); // new
$machine->apply('process');
echo $machine->getState(); // processing
Проблемы: отсутствие встроенного аудита, сложность добавления дополнительных действий при переходе, необходимость ручной сериализации состояния. Решение - добавлять вызовы колбэков или событий внутри метода apply.

Случаи использования: протектирование, простые анкеты, статусы пользователей. Ограничение - плохо поддерживается при росте числа состояний.

Как организовать последовательность действий с цепочкой обязанностей?

Паттерн Chain of Responsibility позволяет последовательно обрабатывать запрос, передавая его по цепочке обработчиков. Это полезно для бизнес-процессов, состоящих из нескольких этапов (например, валидация, расчет скидки, уведомление).


abstract class OrderHandler
{
    private ?OrderHandler $next = null;

    public function setNext(OrderHandler $handler): OrderHandler
    {
        $this->next = $handler;
        return $handler;
    }

    public function handle(Order $order): void
    {
        $result = $this->process($order);
        if ($this->next !== null) {
            $this->next->handle($order);
        }
    }

    abstract protected function process(Order $order): bool;
}

class ValidationHandler extends OrderHandler
{
    protected function process(Order $order): bool
    {
        if (empty($order->getItems())) {
            throw new \Exception('Order has no items');
        }
        return true;
    }
}

class DiscountHandler extends OrderHandler
{
    protected function process(Order $order): bool
    {
        $order->setDiscount($order->getTotal() > 100 ? 10 : 0);
        return true;
    }
}

Цепочка строится явно: $validation->setNext($discount). Каждый обработчик выполняет свою задачу и передает управление дальше.

Типичная ошибка - неправильный порядок обработчиков, когда последующий зависит от результата предыдущего, но порядок нарушен. Решение: четко документировать зависимости. Также возможна потеря исключений - в примере исключение прерывает цепочку, но не обрабатывается. Нужно добавить try/catch внешний.

Цели: валидация, модификация данных, логгирование, отправка уведомлений. Подходит для процессов с четкой последовательностью и возможностью добавления новых шагов без изменения существующих.

Как использовать события для слабосвязанного бизнес-процесса?

Event-Driven подход с Symfony EventDispatcher позволяет разделить фазы процесса на независимые слушатели. Это дает гибкость и возможность легко добавлять новое поведение.


// src/Event/OrderProcessedEvent.php
class OrderProcessedEvent extends \Symfony\Contracts\EventDispatcher\Event
{
    public const NAME = 'order.processed';
    private $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    public function getOrder(): Order
    {
        return $this->order;
    }
}

// src/EventSubscriber/OrderSubscriber.php
class OrderSubscriber implements \Symfony\Component\EventDispatcher\EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            OrderProcessedEvent::NAME => 'onOrderProcessed',
        ];
    }

    public function onOrderProcessed(OrderProcessedEvent $event)
    {
        $order = $event->getOrder();
        // отправка email, логирование
    }
}

В контроллере после обработки заказа диспатчится событие: $eventDispatcher->dispatch(new OrderProcessedEvent($order), OrderProcessedEvent::NAME).

Основная проблема - слушатели выполняются синхронно, что может замедлить ответ пользователю. Решение: использовать очередь сообщений (RabbitMQ, Redis) для асинхронной обработки. Пример с очередью приведен в разделе расширенных примеров.

Случаи использования: уведомления, журналирование, интеграция с CRM, когда действия не критичны для основного потока. Подходит для высоконагруженных систем с микросервисной архитектурой.

Расширенные примеры реализации

В этом разделе рассмотрены нестандартные сценарии, которые могут возникнуть при автоматизации сложных бизнес-процессов.

Пример 1: Workflow с подпроцессами и параллельными состояниями

Symfony Workflow поддерживает marking store с несколькими активами для параллельных процессов. Например, в заказе одновременно могут выполняться сборка и оплата.

Пример

framework:
    workflows:
        order_parallel:
            type: 'workflow'  # несколько активных мест
            # ...
            places:
                - waiting_payment
                - waiting_picking
                - partially_paid
                - partially_picked
                - ready_to_ship
            transitions:
                pay:
                    from: [waiting_payment, partially_paid]
                    to: partially_paid
                pickup:
                    from: [waiting_picking, partially_picked]
                    to: partially_picked
                all_paid:
                    from: partially_paid
                    to: ready_to_ship
                all_picked:
                    from: partially_picked
                    to: ready_to_ship
После применения 'pay' при active местах ['waiting_payment'] получим ['partially_paid'].
При повторном 'pay' (с неоплаченной частью) места ['partially_paid'] останутся, но сумма оплаты увеличится.
Когда все оплачено, 'all_paid' переводит в 'ready_to_ship'.

Пример 2: Асинхронная обработка через RabbitMQ с сохранением контекста процесса

Предположим, после события order.created необходимо выполнить сложную проверку кредитного лимита. Чтобы не блокировать ответ, отправляем сообщение в очередь.

Пример

// src/MessageHandler/CreditCheckHandler.php
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Workflow\WorkflowInterface;

#[AsMessageHandler]
class CreditCheckHandler
{
    public function __invoke(CreditCheckMessage $message, WorkflowInterface $orderWorkflow)
    {
        $order = $message->getOrder();
        // имитация проверки
        $creditOk = rand(0, 1) === 1;
        if ($creditOk) {
            $orderWorkflow->apply($order, 'approve_credit');
        } else {
            $orderWorkflow->apply($order, 'reject_credit');
        }
    }
}
Пример

// Отправка сообщения в контроллере
$bus->dispatch(new CreditCheckMessage($orderId));
После обработки сообщения статус заказа изменится на 'credit_approved' или 'credit_rejected'.
Очередь гарантирует eventual consistency.

Пример 3: Динамическое построение маршрутов на основе бизнес-правил

Использование Expression Language для вычисления следующего состояния в зависимости от условий. Например, при стоимости заказа больше 1000 рублей требуется дополнительное одобрение руководителя.

Пример

framework:
    workflows:
        order_advanced:
            # ...
            transitions:
                approve:
                    from: new
                    to: approved
                    guard: "subject.getTotal() < 1000"
                require_approval:
                    from: new
                    to: pending_approval
                    guard: "subject.getTotal() >= 1000"
При выполнении кода:
if ($workflow->can($order, 'approve')) $workflow->apply($order, 'approve');
elseif ($workflow->can($order, 'require_approval')) $workflow->apply($order, 'require_approval');

Guard-выражения автоматически выбирают нужный переход. Это позволяет бизнес-логике оставаться в конфигурации, а не в коде.

Пример 4: Комбинация цепочки обязанностей с внешними сервисами

Паттерн Chain of Responsibility может вызывать удаленные API. Например, обработчик доставки обращается к сервису такси.

Пример

class DeliveryHandler extends OrderHandler
{
    public function __construct(private HttpClientInterface $httpClient) {}

    protected function process(Order $order): bool
    {
        $response = $this->httpClient->request('POST', 'https://api.delivery.com/request', [
            'json' => ['order_id' => $order->getId()]
        ]);
        if ($response->getStatusCode() !== 200) {
            throw new \RuntimeException('Delivery API error');
        }
        $order->setDeliveryId($response->toArray()['id']);
        return true;
    }
}
После прохождения цепочки у заказа будет назначен идентификатор доставки и, возможно, вызван следующий обработчик (например, уведомление).

PHP-код для бизнес-процессов - comments

En
бизнес процессы php код (php)