$30 off During Our Annual Pro Sale. View Details »

Using the Workflow component for e-commerce

Using the Workflow component for e-commerce

We got the task to make an order API, from open order, to delivered, with payments in between and after. So there are naturally a lot of states, and a lot of transitions where we needed to calculate the prices correctly and handle credit card transfers. Keeping track of all of this, and when we need to do what, ensuring that an order is always up to date, and that it has the data it needs, and that we send good error messages when a user can not do an action, was a challenge for us until we discovered the workflow component. This is a real happy use case story where I will show you how we did this, and how much more straightforward it was for us to build an otherwise complex system using the workflow component.

Michelle Sanver

May 10, 2019
Tweet

More Decks by Michelle Sanver

Other Decks in Programming

Transcript

  1. @michellesanver #phpday
    Using the Workflow
    component for
    e-commerce

    #phpday | 10th of May 2019

    View Slide

  2. @michellesanver #phpday
    “Learn the most by sharing
    your knowledge with others”
    - @coderabbi
    WIIIIE \o/

    View Slide

  3. @michellesanver #phpday
    Using the Workflow
    component for
    e-commerce

    View Slide

  4. @michellesanver #phpday
    Writing Good Code

    View Slide

  5. @michellesanver #phpday
    This talk assumes little
    knowledge

    View Slide

  6. @michellesanver #phpday
    This talk is…
    Open Source

    View Slide

  7. View Slide

  8. @michellesanver #phpday
    Michelle Sanver
    Colour and code addict

    View Slide

  9. @michellesanver #phpday
    Accent!?

    View Slide

  10. @michellesanver #phpday

    View Slide

  11. @michellesanver #phpday
    “It’s like Uber… 

    For shopping”

    View Slide

  12. @michellesanver #phpday
    Order API
    Challenges

    View Slide

  13. @michellesanver #phpday
    Keeping orders/prices
    up to date

    View Slide

  14. @michellesanver #phpday
    Making our
    consumers love us

    (Good error messages and
    documentation)

    View Slide

  15. @michellesanver #phpday
    Handling cancellation of
    an order

    View Slide

  16. @michellesanver #phpday
    Handling a large amount
    of SMS and Push
    notifications

    View Slide

  17. @michellesanver #phpday

    View Slide

  18. @michellesanver #phpday
    State Machines

    View Slide

  19. @michellesanver #phpday
    Why?

    View Slide

  20. @michellesanver #phpday
    GREEN YELLOW RED

    View Slide

  21. @michellesanver #phpday
    GREEN YELLOW RED

    View Slide

  22. @michellesanver #phpday
    GREEN YELLOW RED

    View Slide

  23. @michellesanver #phpday
    GREEN
    YELLOW
    RED

    View Slide

  24. @michellesanver #phpday
    GREEN
    YELLOW
    RED
    Timer

    View Slide

  25. @michellesanver #phpday
    GREEN
    YELLOW
    RED
    Timer
    T1

    View Slide

  26. @michellesanver #phpday
    GREEN
    YELLOW
    RED
    Timer
    T1 T1

    View Slide

  27. @michellesanver #phpday
    GREEN
    YELLOW
    RED
    Timer
    T1 T1
    T1

    View Slide

  28. @michellesanver #phpday
    GREEN
    YELLOW
    RED
    Timer
    T1 T1
    T1
    T0

    View Slide

  29. @michellesanver #phpday
    GREEN
    YELLOW
    RED
    Timer
    T1 T1
    T1
    T0
    T0
    T0

    View Slide

  30. @michellesanver #phpday
    GREEN RED ;) BLUE

    View Slide

  31. @michellesanver #phpday
    GREEN
    YELLOW
    BLUE
    T1
    T2
    Workflow

    View Slide

  32. @michellesanver #phpday
    GREEN
    YELLOW
    BLUE
    T1
    T2
    State Machine
    T3

    View Slide

  33. @michellesanver #phpday
    We already
    think in states
    and workflows

    View Slide

  34. @michellesanver #phpday
    The Order

    State Machine

    View Slide

  35. @michellesanver #phpday
    Happy Workflow

    View Slide

  36. @michellesanver #phpday
    1. An order is created
    Open

    View Slide

  37. @michellesanver #phpday
    2. Items are updated
    Open

    View Slide

  38. @michellesanver #phpday
    3. User sets delivery
    address on order.
    Open

    View Slide

  39. @michellesanver #phpday
    4. User checks out
    Confirmed

    View Slide

  40. @michellesanver #phpday
    5. Pickers get notified
    Confirmed

    View Slide

  41. @michellesanver #phpday
    6. Picker accepts order
    Assigned
    To
    Picker

    View Slide

  42. @michellesanver #phpday
    7. Picker goes shopping
    Picked
    Up

    View Slide

  43. @michellesanver #phpday
    8. Picker delivers order
    Delivered

    View Slide

  44. @michellesanver #phpday
    9. Card gets charged for
    order, picker gets paid
    Delivered

    View Slide

  45. @michellesanver #phpday
    Open Confirmed
    Assigned
    To
    Picker
    Picked
    Up
    Delivered
    Happy Workflow

    View Slide

  46. @michellesanver #phpday
    Cancelled Aborted

    View Slide

  47. @michellesanver #phpday
    Cancelled
    By
    System

    View Slide

  48. @michellesanver #phpday
    Open Confirmed
    Assigned
    To
    Picker
    Picked
    Up
    Delivered
    Cancelled Aborted
    Cancelled
    By
    System

    View Slide

  49. @michellesanver #phpday
    Open Confirmed
    Assigned
    To
    Picker
    Picked
    Up
    Delivered
    Cancelled
    Aborted
    Cancelled
    By
    System

    View Slide

  50. @michellesanver #phpday
    Open Confirmed
    Assigned
    To
    Picker
    Picked
    Up
    Delivered
    Cancelled
    Aborted
    Cancelled
    By
    System
    Cancel Order
    For technical Reasons
    Cancel
    Abort
    Checkout
    Assign to
    picker
    Pick Up
    Deliver
    Cancel
    Cancel Cancel

    View Slide

  51. @michellesanver #phpday
    Open Open
    UpdateItem

    View Slide

  52. @michellesanver #phpday
    State machines
    make business
    logic explicit

    View Slide

  53. @michellesanver #phpday
    Open Confirmed
    Assigned
    To
    Picker
    Picked
    Up
    Delivered
    Cancelled
    Aborted
    Cancelled
    By
    System
    Cancel Order
    For technical Reasons
    Cancel
    Abort
    Checkout
    Assign to
    picker
    Pick Up
    Deliver
    Cancel
    Cancel Cancel

    View Slide

  54. @michellesanver #phpday
    A workflow does not contain all logic

    View Slide

  55. @michellesanver #phpday
    The Workflow
    Component

    View Slide

  56. @michellesanver #phpday
    composer require
    symfony/workflow

    View Slide

  57. @michellesanver #phpday
    Our Workflow

    View Slide

  58. @michellesanver #phpday
    Open Confirmed
    Assigned
    To
    Picker
    Picked
    Up
    Delivered
    Cancelled
    Aborted
    Cancelled
    By
    System
    Cancel Order
    For technical Reasons
    Cancel
    Abort
    Checkout
    Assign to
    picker
    Pick Up
    Deliver
    Cancel
    Cancel Cancel

    View Slide

  59. @michellesanver #phpday
    framework:

    workflows:

    order:

    type: state_machine

    marking_store:

    type: 'single_state'

    arguments:

    - 'state'

    supports:

    - AppBundle\Entity\Order

    places:

    - open

    - confirmed

    - assigned-to-delivery-person

    - picked-up

    - delivered

    - aborted

    - cancelled

    transitions:

    updateItem:

    from: open

    to: open

    View Slide

  60. @michellesanver #phpday
    framework:

    workflows:

    order:

    type: state_machine

    marking_store:

    type: 'single_state'

    arguments:

    - 'state'

    supports:

    - AppBundle\Entity\Order

    View Slide

  61. @michellesanver #phpday
    framework:

    workflows:

    order:

    type: state_machine

    marking_store:

    type: 'single_state'

    arguments:

    - 'state'

    supports:

    - AppBundle\Entity\Order

    View Slide

  62. @michellesanver #phpday
    framework:

    workflows:

    order:

    type: state_machine
    marking_store:

    type: 'single_state'

    arguments:

    - 'state'

    supports:

    - AppBundle\Entity\Order

    View Slide

  63. @michellesanver #phpday
    framework:

    workflows:

    order:

    type: state_machine

    marking_store:
    type: 'single_state'
    arguments:
    - 'state'
    supports:

    - AppBundle\Entity\Order

    View Slide

  64. @michellesanver #phpday
    framework:

    workflows:

    order:

    type: state_machine

    marking_store:

    type: 'single_state'

    arguments:

    - 'state'

    supports:
    - AppBundle\Entity\Order

    View Slide

  65. @michellesanver #phpday
    places:
    - open

    - confirmed

    - assigned-to-picker

    - picked-up

    - delivered

    - aborted

    - cancelled

    - cancelled-by-system

    View Slide

  66. @michellesanver #phpday
    transitions:
    updateItem:

    from: open

    to: open

    View Slide

  67. @michellesanver #phpday
    The Model

    View Slide

  68. @michellesanver #phpday
    public const STATE_OPEN = 'open';
    public const STATE_CONFIRMED = 'confirmed';
    public const STATE_ASSIGNED_TO_PICKER = 'assigned-to-picker';
    public const STATE_PICKED_UP = 'picked-up';
    public const STATE_DELIVERED = 'delivered';
    public const STATE_ABORTED_BY_CUSTOMER = 'aborted';
    public const STATE_CANCELLED_BY_SUPPORT = 'cancelled';
    public const STATE_CANCELLED_BY_SYSTEM = 'cancelled-by-system';

    View Slide

  69. @michellesanver #phpday
    public const STATES = [
    self::STATE_OPEN,
    self::STATE_CONFIRMED,
    self::STATE_ASSIGNED_TO_PICKER,
    self::STATE_PICKED_UP,
    self::STATE_DELIVERED,
    self::STATE_ABORTED_BY_CUSTOMER,
    self::STATE_CANCELLED_BY_SUPPORT,
    self::STATE_CANCELLED_BY_SYSTEM,
    ];

    View Slide

  70. @michellesanver #phpday
    public const TRANSITION_UPDATE_ITEM = 'updateItem';

    View Slide

  71. @michellesanver #phpday
    /**
    * @var string
    */
    private $state = self::STATE_OPEN;

    View Slide

  72. @michellesanver #phpday

    View Slide

  73. @michellesanver #phpday
    Events

    View Slide

  74. @michellesanver #phpday
    Solves Tight Coupling

    View Slide

  75. @michellesanver #phpday
    EventDispatcher

    View Slide

  76. @michellesanver #phpday
    composer require
    symfony/event-dispatcher

    View Slide

  77. @michellesanver #phpday
    The Listener

    View Slide

  78. @michellesanver #phpday
    class OrderWorkflowListener implements EventSubscriberInterface
    {


    }

    View Slide

  79. @michellesanver #phpday
    /**
    * @return array Hashmap of Symfony events this handler cares about
    */
    public static function getSubscribedEvents()
    {
    return [
    // Guard events (Validate whether the transition is allowed at all)
    'workflow.order.guard' => 'guardGeneralOrderWorkflow',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CHANGE_REGION) => 'guardChangeRegion',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CONFIRM_ORDER) => 'guardConfirmOrder',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_PICKER) => 'guardAssignPicker',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_CUSTOMER) => 'guardAssignCustomer',
    // Enter events (The object is about to enter a new place)
    sprintf('workflow.order.enter.%s', Order::STATE_CONFIRMED) => 'enterConfirmed',
    sprintf('workflow.order.enter.%s', Order::STATE_DELIVERED) => 'enterDelivered',
    // Entered events (The object entered a new place)
    sprintf('workflow.order.entered.%s', Order::STATE_PICKED_UP) => 'enteredPickedUp',
    // The object has completed this transition.
    'workflow.order.completed' => 'onCompleted',
    ];
    }

    View Slide

  80. @michellesanver #phpday
    /**
    * @return array Hashmap of Symfony events this handler cares about
    */
    public static function getSubscribedEvents()
    {
    return [
    // Guard events (Validate whether the transition is allowed at all)
    'workflow.order.guard' => 'guardGeneralOrderWorkflow',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CHANGE_REGION) => 'guardChangeRegion',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CONFIRM_ORDER) => 'guardConfirmOrder',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_PICKER) => 'guardAssignPicker',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_CUSTOMER) => 'guardAssignCustomer',
    // Enter events (The object is about to enter a new place)
    sprintf('workflow.order.enter.%s', Order::STATE_CONFIRMED) => 'enterConfirmed',
    sprintf('workflow.order.enter.%s', Order::STATE_DELIVERED) => 'enterDelivered',
    // Entered events (The object entered a new place)
    sprintf('workflow.order.entered.%s', Order::STATE_PICKED_UP) => 'enteredPickedUp',
    // The object has completed this transition.
    'workflow.order.completed' => 'onCompleted',
    ];
    }

    View Slide

  81. @michellesanver #phpday
    /**
    * @return array Hashmap of Symfony events this handler cares about
    */
    public static function getSubscribedEvents()
    {
    return [
    // Guard events (Validate whether the transition is allowed at all)
    'workflow.order.guard' => 'guardGeneralOrderWorkflow',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CHANGE_REGION) => 'guardChangeRegion',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CONFIRM_ORDER) => 'guardConfirmOrder',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_PICKER) => 'guardAssignPicker',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_CUSTOMER) => 'guardAssignCustomer',
    // Enter events (The object is about to enter a new place)
    sprintf('workflow.order.enter.%s', Order::STATE_CONFIRMED) => 'enterConfirmed',
    sprintf('workflow.order.enter.%s', Order::STATE_DELIVERED) => 'enterDelivered',
    // Entered events (The object entered a new place)
    sprintf('workflow.order.entered.%s', Order::STATE_PICKED_UP) => 'enteredPickedUp',
    // The object has completed this transition.
    'workflow.order.completed' => 'onCompleted',
    ];
    }

    View Slide

  82. @michellesanver #phpday
    /**
    * @return array Hashmap of Symfony events this handler cares about
    */
    public static function getSubscribedEvents()
    {
    return [
    // Guard events (Validate whether the transition is allowed at all)
    'workflow.order.guard' => 'guardGeneralOrderWorkflow',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CHANGE_REGION) => 'guardChangeRegion',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CONFIRM_ORDER) => 'guardConfirmOrder',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_PICKER) => 'guardAssignPicker',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_CUSTOMER) => 'guardAssignCustomer',
    // Enter events (The object is about to enter a new place)
    sprintf('workflow.order.enter.%s', Order::STATE_CONFIRMED) => 'enterConfirmed',
    sprintf('workflow.order.enter.%s', Order::STATE_DELIVERED) => 'enterDelivered',
    // Entered events (The object entered a new place)
    sprintf('workflow.order.entered.%s', Order::STATE_PICKED_UP) => 'enteredPickedUp',
    // The object has completed this transition.
    'workflow.order.completed' => 'onCompleted',
    ];
    }

    View Slide

  83. @michellesanver #phpday
    /**
    * @return array Hashmap of Symfony events this handler cares about
    */
    public static function getSubscribedEvents()
    {
    return [
    // Guard events (Validate whether the transition is allowed at all)
    'workflow.order.guard' => 'guardGeneralOrderWorkflow',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CHANGE_REGION) => 'guardChangeRegion',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_CONFIRM_ORDER) => 'guardConfirmOrder',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_PICKER) => 'guardAssignPicker',
    sprintf('workflow.order.guard.%s', Order::TRANSITION_ASSIGN_CUSTOMER) => 'guardAssignCustomer',
    // Enter events (The object is about to enter a new place)
    sprintf('workflow.order.enter.%s', Order::STATE_CONFIRMED) => 'enterConfirmed',
    sprintf('workflow.order.enter.%s', Order::STATE_DELIVERED) => 'enterDelivered',
    // Entered events (The object entered a new place)
    sprintf('workflow.order.entered.%s', Order::STATE_PICKED_UP) => 'enteredPickedUp',
    // The object has completed this transition.
    'workflow.order.completed' => 'onCompleted',
    ];
    }

    View Slide

  84. @michellesanver #phpday
    Workflow
    component does so
    much for us

    View Slide

  85. @michellesanver #phpday
    Order API
    Challenges Solved

    View Slide

  86. @michellesanver #phpday
    Keeping orders/prices
    up to date

    View Slide

  87. @michellesanver #phpday
    public function onCompleted(Event $event): void
    {
    $transition = $event->getTransition();
    $fromState = $transition->getFroms()[0];
    /** @var Order $order */
    $order = $event->getSubject();
    // If an order is already in delivered state, all calculations are done, and should stay as
    they are.
    if (Order::STATE_DELIVERED !== $fromState) {
    $this->orderModifier->updateTotals($order);
    }
    $this->orderStateHistoryRepository->storeOrderStateTransition($order, $transition);
    $this->orderEventService->eventHappened($transition->getName(), $order->getId(), $fromState);
    }

    View Slide

  88. @michellesanver #phpday
    public function onCompleted(Event $event): void
    {
    $transition = $event->getTransition();
    $fromState = $transition->getFroms()[0];
    /** @var Order $order */
    $order = $event->getSubject();
    // If an order is already in delivered state, all calculations are done, and should stay as
    they are.
    if (Order::STATE_DELIVERED !== $fromState) {
    $this->orderModifier->updateTotals($order);
    }
    $this->orderStateHistoryRepository->storeOrderStateTransition($order, $transition);
    $this->orderEventService->eventHappened($transition->getName(), $order->getId(), $fromState);
    }

    View Slide

  89. @michellesanver #phpday
    if (Order::STATE_DELIVERED !== $fromState) {
    $this->orderModifier->updateTotals($order);
    }

    View Slide

  90. @michellesanver #phpday
    Making our
    consumers love us

    (Good error messages and
    documentation)

    View Slide

  91. @michellesanver #phpday
    Handling cancellation of
    an order

    View Slide

  92. @michellesanver #phpday
    Handling a large amount
    of SMS and Push
    notifications

    View Slide

  93. @michellesanver #phpday
    public function onCompleted(Event $event): void
    {
    $transition = $event->getTransition();
    $fromState = $transition->getFroms()[0];
    /** @var Order $order */
    $order = $event->getSubject();
    // If an order is already in delivered state, all calculations are done, and should stay as
    they are.
    if (Order::STATE_DELIVERED !== $fromState) {
    $this->orderModifier->updateTotals($order);
    }
    $this->orderStateHistoryRepository->storeOrderStateTransition($order, $transition);
    $this->orderEventService->eventHappened($transition->getName(), $order->getId(), $fromState);
    }

    View Slide

  94. @michellesanver #phpday
    public function onCompleted(Event $event): void
    {
    $transition = $event->getTransition();
    $fromState = $transition->getFroms()[0];
    /** @var Order $order */
    $order = $event->getSubject();
    // If an order is already in delivered state, all calculations are done, and should stay as
    they are.
    if (Order::STATE_DELIVERED !== $fromState) {
    $this->orderModifier->updateTotals($order);
    }
    $this->orderStateHistoryRepository->storeOrderStateTransition($order, $transition);
    $this->orderEventService->eventHappened($transition->getName(), $order->getId(), $fromState);
    }

    View Slide

  95. @michellesanver #phpday
    $this->orderEventService->eventHappened($transition->getName(), $order->getId(), $fromState);

    View Slide

  96. @michellesanver #phpday
    public function eventHappened(string $type, string $orderId, ?string $previousState)
    {
    foreach ($this->handlers as $handler) {
    if ($handler->canHandle($type)) {
    $this->orderEventProducer->publish(
    $type,
    $orderId,
    $previousState,
    $handler->getPriority()
    );
    return;
    }
    }
    }

    View Slide

  97. @michellesanver #phpday
    public function eventHappened(string $type, string $orderId, ?string $previousState)
    {
    foreach ($this->handlers as $handler) {
    if ($handler->canHandle($type)) {
    $this->orderEventProducer->publish(
    $type,
    $orderId,
    $previousState,
    $handler->getPriority()
    );
    return;
    }
    }
    }

    View Slide

  98. @michellesanver #phpday
    class SmsNotificationHandler implements OrderEventHandler
    {
    private const TRANSITION_STATE_MAP = [
    Order::TRANSITION_ASSIGN_PICKER => Order::STATE_ASSIGNED_TO_PICKER,
    Order::TRANSITION_TIMEOUT_FINDING_PICKER => Order::STATE_ABORTED_BY_CUSTOMER,
    Order::TRANSITION_PICKER_CONFIRMS_PURCHASE => Order::STATE_PICKED_UP,
    Order::TRANSITION_CONFIRM_DELIVERY => Order::STATE_DELIVERED,
    Order::TRANSITION_REFERRAL_PICKER_RECEIVES_REWARD => Order::STATE_DELIVERED,
    Order::TRANSITION_SEND_ORDER_AVAILABLE_REMINDER => Order::STATE_CONFIRMED,
    ];

    View Slide

  99. @michellesanver #phpday
    public function canHandle(string $type): bool
    {
    return array_key_exists($type, self::TRANSITION_STATE_MAP);
    }

    View Slide

  100. @michellesanver #phpday
    Conclusion

    View Slide

  101. @michellesanver #phpday
    We already
    think in states
    and workflows

    View Slide

  102. @michellesanver #phpday
    State machines
    make business
    logic explicit

    View Slide

  103. @michellesanver #phpday
    Workflow
    component does so
    much for us!

    View Slide

  104. @michellesanver #phpday
    Writing Good Code

    View Slide

  105. @michellesanver #phpday
    This talk is for you
    Questions?
    Contributions?

    View Slide

  106. @michellesanver #phpday
    Thank You
    #phpday!
    https://joind.in/talk/caf64

    View Slide