Создание бизнес-логики на 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 выполняет его. Это предотвращает недопустимые изменения статуса.
Цель использования: управление сложными процессами с множеством статусов (заказы, документы, заявки) с гарантией целостности. Подходит для средних и крупных проектов.
Как создать простой конечный автомат без внешних библиотек?
Для небольших проектов или 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
Случаи использования: протектирование, простые анкеты, статусы пользователей. Ограничение - плохо поддерживается при росте числа состояний.
Как организовать последовательность действий с цепочкой обязанностей?
Паттерн 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). Каждый обработчик выполняет свою задачу и передает управление дальше.
Цели: валидация, модификация данных, логгирование, отправка уведомлений. Подходит для процессов с четкой последовательностью и возможностью добавления новых шагов без изменения существующих.
Как использовать события для слабосвязанного бизнес-процесса?
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).
Случаи использования: уведомления, журналирование, интеграция с 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;
}
}
После прохождения цепочки у заказа будет назначен идентификатор доставки и, возможно, вызван следующий обработчик (например, уведомление).