BPMN -- стандартная нотация для моделирования бизнес-процессов. Разработана OMG (Object Management Group). Понятна как бизнес-аналитикам, так и разработчикам. Цель -- единый язык между бизнесом и IT.
<?php
declare(strict_types=1);
namespace App\Workflow;
/**
* State machine that models a BPMN process in code.
* For production, use Symfony Workflow component or Camunda.
*/
enum ProcessState: string
{
case OrderReceived = 'order_received';
case Validating = 'validating';
case PaymentProcessing = 'payment_processing';
case PaymentFailed = 'payment_failed';
case Picking = 'picking';
case Packing = 'packing';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Rejected = 'rejected';
}
final class OrderWorkflow
{
/** @var array<string, array<string, string>> */
private const TRANSITIONS = [
'order_received' => [
'validate' => 'validating',
],
'validating' => [
'approve' => 'payment_processing',
'reject' => 'rejected',
],
'payment_processing' => [
'payment_success' => 'picking',
'payment_failure' => 'payment_failed',
],
'payment_failed' => [
'retry_payment' => 'payment_processing',
'cancel' => 'rejected',
],
'picking' => [
'picked' => 'packing',
],
'packing' => [
'packed' => 'shipped',
],
'shipped' => [
'delivered' => 'delivered',
],
];
/**
* Execute a transition in the workflow.
*/
public function transition(ProcessState $current, string $action): ProcessState
{
$allowed = self::TRANSITIONS[$current->value] ?? [];
if (!isset($allowed[$action])) {
throw new WorkflowException(
sprintf(
'Action "%s" is not allowed in state "%s". Allowed: %s',
$action,
$current->value,
implode(', ', array_keys($allowed)),
),
);
}
return ProcessState::from($allowed[$action]);
}
/**
* Get all available transitions from current state.
*
* @return array<string, string> action => target state
*/
public function getAvailableTransitions(ProcessState $current): array
{
return self::TRANSITIONS[$current->value] ?? [];
}
/**
* Check if process is in a terminal state.
*/
public function isTerminal(ProcessState $state): bool
{
return in_array($state, [ProcessState::Delivered, ProcessState::Rejected], true);
}
/**
* Visualize workflow as text (for debugging).
*/
public function describe(): string
{
$lines = [];
foreach (self::TRANSITIONS as $from => $transitions) {
foreach ($transitions as $action => $to) {
$lines[] = sprintf(' [%s] --%s--> [%s]', $from, $action, $to);
}
}
return "Workflow:\n" . implode("\n", $lines);
}
}
package workflow
import (
"fmt"
"strings"
)
// ProcessState represents a state in the order workflow.
type ProcessState string
const (
StateOrderReceived ProcessState = "order_received"
StateValidating ProcessState = "validating"
StatePaymentProcessing ProcessState = "payment_processing"
StatePaymentFailed ProcessState = "payment_failed"
StatePicking ProcessState = "picking"
StatePacking ProcessState = "packing"
StateShipped ProcessState = "shipped"
StateDelivered ProcessState = "delivered"
StateRejected ProcessState = "rejected"
)
// transitions maps state -> action -> next state.
var workflowTransitions = map[ProcessState]map[string]ProcessState{
StateOrderReceived: {"validate": StateValidating},
StateValidating: {"approve": StatePaymentProcessing, "reject": StateRejected},
StatePaymentProcessing: {"payment_success": StatePicking, "payment_failure": StatePaymentFailed},
StatePaymentFailed: {"retry_payment": StatePaymentProcessing, "cancel": StateRejected},
StatePicking: {"picked": StatePacking},
StatePacking: {"packed": StateShipped},
StateShipped: {"delivered": StateDelivered},
}
// OrderWorkflow models a BPMN process as a state machine.
type OrderWorkflow struct{}
// Transition applies an action and returns the next state.
func (OrderWorkflow) Transition(current ProcessState, action string) (ProcessState, error) {
allowed, ok := workflowTransitions[current]
if !ok {
return "", fmt.Errorf("no transitions from state %q", current)
}
next, ok := allowed[action]
if !ok {
actions := make([]string, 0, len(allowed))
for a := range allowed {
actions = append(actions, a)
}
return "", fmt.Errorf("action %q not allowed in state %q; allowed: %s",
action, current, strings.Join(actions, ", "))
}
return next, nil
}
// GetAvailableTransitions returns possible actions from the current state.
func (OrderWorkflow) GetAvailableTransitions(current ProcessState) map[string]ProcessState {
return workflowTransitions[current]
}
// IsTerminal returns true if the state is a final state.
func (OrderWorkflow) IsTerminal(state ProcessState) bool {
return state == StateDelivered || state == StateRejected
}
// Describe returns a text visualization of the workflow.
func (OrderWorkflow) Describe() string {
var sb strings.Builder
sb.WriteString("Workflow:\n")
for from, transitions := range workflowTransitions {
for action, to := range transitions {
sb.WriteString(fmt.Sprintf(" [%s] --%s--> [%s]\n", from, action, to))
}
}
return sb.String()
}
using System.Text;
namespace App.Workflow;
/// Represents a state in the order workflow.
public enum ProcessState
{
OrderReceived,
Validating,
PaymentProcessing,
PaymentFailed,
Picking,
Packing,
Shipped,
Delivered,
Rejected,
}
/// Available actions that drive the workflow forward.
public enum WorkflowAction
{
Validate,
Approve,
Reject,
PaymentSuccess,
PaymentFailure,
RetryPayment,
Cancel,
Picked,
Packed,
Delivered,
}
public sealed class WorkflowException : Exception
{
public WorkflowException(string message) : base(message) { }
}
/// State machine that models a BPMN process in code.
/// For production, use a workflow engine such as Elsa or Camunda.
public sealed class OrderWorkflow
{
private static readonly IReadOnlyDictionary<ProcessState, IReadOnlyDictionary<WorkflowAction, ProcessState>> Transitions =
new Dictionary<ProcessState, IReadOnlyDictionary<WorkflowAction, ProcessState>>
{
[ProcessState.OrderReceived] = new Dictionary<WorkflowAction, ProcessState>
{
[WorkflowAction.Validate] = ProcessState.Validating,
},
[ProcessState.Validating] = new Dictionary<WorkflowAction, ProcessState>
{
[WorkflowAction.Approve] = ProcessState.PaymentProcessing,
[WorkflowAction.Reject] = ProcessState.Rejected,
},
[ProcessState.PaymentProcessing] = new Dictionary<WorkflowAction, ProcessState>
{
[WorkflowAction.PaymentSuccess] = ProcessState.Picking,
[WorkflowAction.PaymentFailure] = ProcessState.PaymentFailed,
},
[ProcessState.PaymentFailed] = new Dictionary<WorkflowAction, ProcessState>
{
[WorkflowAction.RetryPayment] = ProcessState.PaymentProcessing,
[WorkflowAction.Cancel] = ProcessState.Rejected,
},
[ProcessState.Picking] = new Dictionary<WorkflowAction, ProcessState>
{
[WorkflowAction.Picked] = ProcessState.Packing,
},
[ProcessState.Packing] = new Dictionary<WorkflowAction, ProcessState>
{
[WorkflowAction.Packed] = ProcessState.Shipped,
},
[ProcessState.Shipped] = new Dictionary<WorkflowAction, ProcessState>
{
[WorkflowAction.Delivered] = ProcessState.Delivered,
},
};
// Execute a transition in the workflow.
public ProcessState Transition(ProcessState current, WorkflowAction action)
{
var allowed = GetAvailableTransitions(current);
if (!allowed.TryGetValue(action, out var next))
{
throw new WorkflowException(
$"Action \"{action}\" is not allowed in state \"{current}\". " +
$"Allowed: {string.Join(", ", allowed.Keys)}");
}
return next;
}
// Get all available transitions from the current state.
public IReadOnlyDictionary<WorkflowAction, ProcessState> GetAvailableTransitions(ProcessState current)
=> Transitions.TryGetValue(current, out var allowed)
? allowed
: new Dictionary<WorkflowAction, ProcessState>();
// Check whether the process reached a terminal state.
public bool IsTerminal(ProcessState state)
=> state is ProcessState.Delivered or ProcessState.Rejected;
// Visualize the workflow as text (for debugging).
public string Describe()
{
var builder = new StringBuilder("Workflow:").AppendLine();
foreach (var (from, transitions) in Transitions)
{
foreach (var (action, to) in transitions)
{
builder.AppendLine($" [{from}] --{action}--> [{to}]");
}
}
return builder.ToString();
}
}
from enum import Enum
from types import MappingProxyType
from typing import Mapping
class ProcessState(str, Enum):
"""Represents a state in the order workflow."""
ORDER_RECEIVED = "order_received"
VALIDATING = "validating"
PAYMENT_PROCESSING = "payment_processing"
PAYMENT_FAILED = "payment_failed"
PICKING = "picking"
PACKING = "packing"
SHIPPED = "shipped"
DELIVERED = "delivered"
REJECTED = "rejected"
class WorkflowAction(str, Enum):
"""Available actions that drive the workflow forward."""
VALIDATE = "validate"
APPROVE = "approve"
REJECT = "reject"
PAYMENT_SUCCESS = "payment_success"
PAYMENT_FAILURE = "payment_failure"
RETRY_PAYMENT = "retry_payment"
CANCEL = "cancel"
PICKED = "picked"
PACKED = "packed"
DELIVERED = "delivered"
class WorkflowError(Exception):
"""Raised when a transition is not allowed."""
TERMINAL_STATES = frozenset({ProcessState.DELIVERED, ProcessState.REJECTED})
class OrderWorkflow:
"""State machine that models a BPMN process in code.
For production, use a workflow engine such as Temporal or Camunda.
"""
# MappingProxyType makes the transition table read-only at runtime.
_TRANSITIONS: Mapping[ProcessState, Mapping[WorkflowAction, ProcessState]] = MappingProxyType(
{
ProcessState.ORDER_RECEIVED: MappingProxyType(
{WorkflowAction.VALIDATE: ProcessState.VALIDATING}
),
ProcessState.VALIDATING: MappingProxyType(
{
WorkflowAction.APPROVE: ProcessState.PAYMENT_PROCESSING,
WorkflowAction.REJECT: ProcessState.REJECTED,
}
),
ProcessState.PAYMENT_PROCESSING: MappingProxyType(
{
WorkflowAction.PAYMENT_SUCCESS: ProcessState.PICKING,
WorkflowAction.PAYMENT_FAILURE: ProcessState.PAYMENT_FAILED,
}
),
ProcessState.PAYMENT_FAILED: MappingProxyType(
{
WorkflowAction.RETRY_PAYMENT: ProcessState.PAYMENT_PROCESSING,
WorkflowAction.CANCEL: ProcessState.REJECTED,
}
),
ProcessState.PICKING: MappingProxyType(
{WorkflowAction.PICKED: ProcessState.PACKING}
),
ProcessState.PACKING: MappingProxyType(
{WorkflowAction.PACKED: ProcessState.SHIPPED}
),
ProcessState.SHIPPED: MappingProxyType(
{WorkflowAction.DELIVERED: ProcessState.DELIVERED}
),
}
)
def transition(self, current: ProcessState, action: WorkflowAction) -> ProcessState:
"""Execute a transition in the workflow."""
allowed = self.get_available_transitions(current)
if action not in allowed:
allowed_names = ", ".join(a.value for a in allowed)
raise WorkflowError(
f'Action "{action.value}" is not allowed in state '
f'"{current.value}". Allowed: {allowed_names}'
)
return allowed[action]
def get_available_transitions(
self,
current: ProcessState,
) -> Mapping[WorkflowAction, ProcessState]:
"""Get all available transitions from the current state."""
return self._TRANSITIONS.get(current, MappingProxyType({}))
@staticmethod
def is_terminal(state: ProcessState) -> bool:
"""Check whether the process reached a terminal state."""
return state in TERMINAL_STATES
def describe(self) -> str:
"""Visualize the workflow as text (for debugging)."""
lines = [
f" [{from_state.value}] --{action.value}--> [{to_state.value}]"
for from_state, transitions in self._TRANSITIONS.items()
for action, to_state in transitions.items()
]
return "Workflow:\n" + "\n".join(lines)
### Symfony Workflow Integration
<?php
declare(strict_types=1);
namespace App\Workflow;
// config/packages/workflow.yaml concept:
// framework:
// workflows:
// order:
// type: state_machine
// marking_store:
// type: method
// property: status
// supports: [App\Entity\Order]
// places: [received, validating, processing_payment, picking, shipped, delivered, rejected]
// transitions:
// validate: { from: received, to: validating }
// approve: { from: validating, to: processing_payment }
// reject: { from: validating, to: rejected }
// payment_ok: { from: processing_payment, to: picking }
// pick: { from: picking, to: shipped }
// deliver: { from: shipped, to: delivered }
use Symfony\Component\Workflow\WorkflowInterface;
final readonly class OrderWorkflowService
{
public function __construct(
private WorkflowInterface $orderStateMachine,
) {}
/**
* Apply transition with validation.
*/
public function apply(Order $order, string $transition): void
{
if (!$this->orderStateMachine->can($order, $transition)) {
$allowed = array_map(
static fn($t) => $t->getName(),
$this->orderStateMachine->getEnabledTransitions($order),
);
throw new WorkflowException(sprintf(
'Cannot apply "%s". Allowed: %s',
$transition,
implode(', ', $allowed),
));
}
$this->orderStateMachine->apply($order, $transition);
}
}
package workflow
import "fmt"
// OrderWorkflowService wraps the state machine with validation.
// In Go, you can use libraries like github.com/looplab/fsm for production.
type OrderWorkflowService struct {
workflow OrderWorkflow
}
// NewOrderWorkflowService creates a workflow service.
func NewOrderWorkflowService() *OrderWorkflowService {
return &OrderWorkflowService{workflow: OrderWorkflow{}}
}
// Apply validates and applies a transition to an order.
func (s *OrderWorkflowService) Apply(currentState ProcessState, transition string) (ProcessState, error) {
allowed := s.workflow.GetAvailableTransitions(currentState)
next, ok := allowed[transition]
if !ok {
names := make([]string, 0, len(allowed))
for action := range allowed {
names = append(names, action)
}
return "", fmt.Errorf("cannot apply %q; allowed: %v", transition, names)
}
return next, nil
}
namespace App.Workflow;
/// Wraps the state machine with validation and persistence.
/// In .NET, a production workflow would typically use Elsa or
/// Microsoft.Extensions.Workflows instead of a hand-rolled table.
public sealed class OrderWorkflowService
{
private readonly OrderWorkflow _workflow;
private readonly IOrderRepository _orders;
public OrderWorkflowService(OrderWorkflow workflow, IOrderRepository orders)
{
_workflow = workflow;
_orders = orders;
}
// Apply a transition with validation, then persist the new state.
public ProcessState Apply(Order order, WorkflowAction action)
{
var allowed = _workflow.GetAvailableTransitions(order.State);
if (!allowed.ContainsKey(action))
{
throw new WorkflowException(
$"Cannot apply \"{action}\". Allowed: {string.Join(", ", allowed.Keys)}");
}
var next = _workflow.Transition(order.State, action);
order.State = next;
_orders.Save(order);
return next;
}
}
from typing import Protocol
class OrderRepository(Protocol):
# Python has no interfaces; typing.Protocol describes what the
# service needs from storage without binding it to an ORM.
def save(self, order: Order) -> None: ...
class OrderWorkflowService:
"""Wrap the state machine with validation and persistence.
In production, a workflow engine such as Temporal would own the
transition table and the durable execution state.
"""
# Python has no compile-time DI container; dependencies are injected manually.
def __init__(self, workflow: OrderWorkflow, orders: OrderRepository) -> None:
self._workflow = workflow
self._orders = orders
def apply(self, order: Order, action: WorkflowAction) -> ProcessState:
"""Apply a transition with validation, then persist the new state."""
allowed = self._workflow.get_available_transitions(order.state)
if action not in allowed:
allowed_names = ", ".join(a.value for a in allowed)
raise WorkflowError(
f'Cannot apply "{action.value}". Allowed: {allowed_names}'
)
order.state = self._workflow.transition(order.state, action)
self._orders.save(order)
return order.state
## Сравнение BPMN и Activity Diagram (UML)
Аспект
BPMN
Activity Diagram
Фокус
Бизнес-процессы
Алгоритмы, потоки
Аудитория
Бизнес + IT
IT
Events
Богатая система событий
Ограниченная
Pools/Lanes
Да (участники процесса)
Swimlanes
Стандарт
ISO 19510
UML спецификация
Исполняемость
Можно выполнить в BPM engine
Только визуализация
Инструменты BPMN
Инструмент
Тип
Исполняемый
Camunda
BPM platform
Да
Bizagi Modeler
Моделирование
Нет (бесплатный)
BPMN.io
Web-редактор
Нет (open-source)
draw.io
Универсальный
Нет
Signavio
Enterprise
Да
Итоги
Концепция
Суть
Events
Начало, конец, промежуточные события процесса
Activities
Задачи: user, service, script, sub-process
Gateways
XOR (один), AND (все), OR (один+)
Pools/Lanes
Участники и роли в процессе
Message Flow
Коммуникация между участниками
BPM Engine
Автоматическое исполнение BPMN-процессов
Совет: BPMN отлично подходит для документирования процессов при передаче знаний. Новый сотрудник прочитает BPMN-диаграмму быстрее, чем текстовое описание процесса.