Diese Präsentation wurde erfolgreich gemeldet.
Wir verwenden Ihre LinkedIn Profilangaben und Informationen zu Ihren Aktivitäten, um Anzeigen zu personalisieren und Ihnen relevantere Inhalte anzuzeigen. Sie können Ihre Anzeigeneinstellungen jederzeit ändern.

Michelle Sanver "Using the Workflow component for e-commerce"

100 Aufrufe

Veröffentlicht am

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.

Veröffentlicht in: Technologie
  • Als Erste(r) kommentieren

  • Gehören Sie zu den Ersten, denen das gefällt!

Michelle Sanver "Using the Workflow component for e-commerce"

  1. 1. Using the Workflow component for e-commerce Michelle Sanver Liip
  2. 2. @michellesanver #phpfwdays “Learn the most by sharing your knowledge with others” - @coderabbi WIIIIE o/
  3. 3. @michellesanver #phpfwdays Using the Workflow component for e-commerce
  4. 4. @michellesanver #phpfwdays Writing Good Code
  5. 5. @michellesanver #phpfwdays This talk assumes little knowledge
  6. 6. @michellesanver #phpfwdays This talk is… Open Source
  7. 7. @michellesanver #phpfwdays Michelle Sanver Colour and code addict
  8. 8. @michellesanver #phpfwdays Accent!?
  9. 9. @michellesanver #phpfwdays
  10. 10. @michellesanver #phpfwdays “It’s like Uber… 
 For shopping”
  11. 11. @michellesanver #phpfwdays Order API Challenges
  12. 12. @michellesanver #phpfwdays Keeping orders/prices up to date
  13. 13. @michellesanver #phpfwdays Making our consumers love us
 (Good error messages and documentation)
  14. 14. @michellesanver #phpfwdays Handling cancellation of an order
  15. 15. @michellesanver #phpfwdays Handling a large amount of SMS and Push notifications
  16. 16. @michellesanver #phpfwdays
  17. 17. @michellesanver #phpfwdays State Machines
  18. 18. @michellesanver #phpfwdays Why?
  19. 19. @michellesanver #phpfwdays GREEN YELLOW RED
  20. 20. @michellesanver #phpfwdays GREEN YELLOW RED
  21. 21. @michellesanver #phpfwdays GREEN YELLOW RED
  22. 22. @michellesanver #phpfwdays GREEN YELLOW RED
  23. 23. @michellesanver #phpfwdays GREEN YELLOW REDTimer
  24. 24. @michellesanver #phpfwdays GREEN YELLOW REDTimer T1
  25. 25. @michellesanver #phpfwdays GREEN YELLOW REDTimer T1 T1
  26. 26. @michellesanver #phpfwdays GREEN YELLOW REDTimer T1 T1 T1
  27. 27. @michellesanver #phpfwdays GREEN YELLOW RED Timer T1 T1 T1 T0
  28. 28. @michellesanver #phpfwdays GREEN YELLOW RED Timer T1 T1 T1 T0 T0 T0
  29. 29. @michellesanver #phpfwdays GREEN RED ;) BLUE
  30. 30. @michellesanver #phpfwdays GREEN YELLOW BLUE T1 T2 Workflow
  31. 31. @michellesanver #phpfwdays GREEN YELLOW BLUE T1 T2 State Machine T3
  32. 32. @michellesanver #phpfwdays We already think in states and workflows
  33. 33. @michellesanver #phpfwdays The Order
 State Machine
  34. 34. @michellesanver #phpfwdays Happy Workflow
  35. 35. @michellesanver #phpfwdays 1. An order is created Open
  36. 36. @michellesanver #phpfwdays 2. Items are updated Open
  37. 37. @michellesanver #phpfwdays 3. User sets delivery address on order. Open
  38. 38. @michellesanver #phpfwdays 4. User checks out Confirmed
  39. 39. @michellesanver #phpfwdays 5. Pickers get notified Confirmed
  40. 40. @michellesanver #phpfwdays 6. Picker accepts order Assigned To Picker
  41. 41. @michellesanver #phpfwdays 7. Picker goes shopping Picked Up
  42. 42. @michellesanver #phpfwdays 8. Picker delivers order Delivered
  43. 43. @michellesanver #phpfwdays 9. Card gets charged for order, picker gets paid Delivered
  44. 44. @michellesanver #phpfwdays Open Confirmed Assigned To Picker Picked Up Delivered Happy Workflow
  45. 45. @michellesanver #phpfwdays Cancelled Aborted
  46. 46. @michellesanver #phpfwdays Cancelled By System
  47. 47. @michellesanver #phpfwdays Open Confirmed Assigned To Picker Picked Up Delivered Cancelled Aborted Cancelled By System
  48. 48. @michellesanver #phpfwdays Open Confirmed Assigned To Picker Picked Up Delivered CancelledAborted Cancelled By System
  49. 49. @michellesanver #phpfwdays Open Confirmed Assigned To Picker Picked Up Delivered CancelledAborted Cancelled By System Cancel Order For technical Reasons Cancel Abort Checkout Assign to picker Pick Up Deliver Cancel Cancel Cancel
  50. 50. @michellesanver #phpfwdays Open OpenUpdateItem
  51. 51. @michellesanver #phpfwdays State machines make business logic explicit
  52. 52. @michellesanver #phpfwdays Open Confirmed Assigned To Picker Picked Up Delivered CancelledAborted Cancelled By System Cancel Order For technical Reasons Cancel Abort Checkout Assign to picker Pick Up Deliver Cancel Cancel Cancel
  53. 53. @michellesanver #phpfwdays A workflow does not contain all logic
  54. 54. @michellesanver #phpfwdays The Workflow Component
  55. 55. @michellesanver #phpfwdays composer require symfony/workflow
  56. 56. @michellesanver #phpfwdays Our Workflow
  57. 57. @michellesanver #phpfwdays Open Confirmed Assigned To Picker Picked Up Delivered CancelledAborted Cancelled By System Cancel Order For technical Reasons Cancel Abort Checkout Assign to picker Pick Up Deliver Cancel Cancel Cancel
  58. 58. @michellesanver #phpfwdays framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundleEntityOrder places: - open - confirmed - assigned-to-delivery-person - picked-up - delivered - aborted - cancelled transitions: updateItem: from: open to: open
  59. 59. @michellesanver #phpfwdays framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundleEntityOrder
  60. 60. @michellesanver #phpfwdays framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundleEntityOrder
  61. 61. @michellesanver #phpfwdays framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundleEntityOrder
  62. 62. @michellesanver #phpfwdays framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundleEntityOrder
  63. 63. @michellesanver #phpfwdays framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundleEntityOrder
  64. 64. @michellesanver #phpfwdays places: - open - confirmed - assigned-to-picker - picked-up - delivered - aborted - cancelled - cancelled-by-system
  65. 65. @michellesanver #phpfwdays transitions: updateItem: from: open to: open
  66. 66. @michellesanver #phpfwdays The Model
  67. 67. @michellesanver #phpfwdays 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';
  68. 68. @michellesanver #phpfwdays 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, ];
  69. 69. @michellesanver #phpfwdays public const TRANSITION_UPDATE_ITEM = 'updateItem';
  70. 70. @michellesanver #phpfwdays /** * @var string */ private $state = self::STATE_OPEN;
  71. 71. @michellesanver #phpfwdays
  72. 72. @michellesanver #phpfwdays Demo: Workflow outside of Symfony
  73. 73. @michellesanver #phpfwdays Events
  74. 74. @michellesanver #phpfwdays EventDispatcher
  75. 75. @michellesanver #phpfwdays EventDispatcher Listener1 Listener2
  76. 76. @michellesanver #phpfwdays EventDispatcher Listener1 Listener2
  77. 77. @michellesanver #phpfwdays EventDispatcher Listener1 Listener2 Event1 Event2
  78. 78. @michellesanver #phpfwdays EventDispatcher Listener1 Listener2 Event1 Event2
  79. 79. @michellesanver #phpfwdays Solves Tight Coupling
  80. 80. @michellesanver #phpfwdays EventDispatcher
  81. 81. @michellesanver #phpfwdays composer require symfony/event-dispatcher
  82. 82. @michellesanver #phpfwdays The Listener
  83. 83. @michellesanver #phpfwdays class OrderWorkflowListener implements EventSubscriberInterface {
 … }
  84. 84. @michellesanver #phpfwdays /** * @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', ]; }
  85. 85. @michellesanver #phpfwdays /** * @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', ]; }
  86. 86. @michellesanver #phpfwdays /** * @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', ]; }
  87. 87. @michellesanver #phpfwdays /** * @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', ]; }
  88. 88. @michellesanver #phpfwdays /** * @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', ]; }
  89. 89. @michellesanver #phpfwdays Workflow component does so much for us
  90. 90. @michellesanver #phpfwdays Order API Challenges Solved
  91. 91. @michellesanver #phpfwdays Keeping orders/prices up to date
  92. 92. @michellesanver #phpfwdays 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); }
  93. 93. @michellesanver #phpfwdays 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); }
  94. 94. @michellesanver #phpfwdays if (Order::STATE_DELIVERED !== $fromState) { $this->orderModifier->updateTotals($order); }
  95. 95. @michellesanver #phpfwdays Making our consumers love us
 (Good error messages and documentation)
  96. 96. @michellesanver #phpfwdays Handling cancellation of an order
  97. 97. @michellesanver #phpfwdays Handling a large amount of SMS and Push notifications
  98. 98. @michellesanver #phpfwdays 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); }
  99. 99. @michellesanver #phpfwdays 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); }
  100. 100. @michellesanver #phpfwdays $this->orderEventService->eventHappened($transition->getName(), $order->getId(), $fromState);
  101. 101. @michellesanver #phpfwdays 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; } } }
  102. 102. @michellesanver #phpfwdays 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; } } }
  103. 103. @michellesanver #phpfwdays 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, ];
  104. 104. @michellesanver #phpfwdays public function canHandle(string $type): bool { return array_key_exists($type, self::TRANSITION_STATE_MAP); }
  105. 105. @michellesanver #phpfwdays Conclusion
  106. 106. @michellesanver #phpfwdays We already think in states and workflows
  107. 107. @michellesanver #phpfwdays State machines make business logic explicit
  108. 108. @michellesanver #phpfwdays Workflow component does so much for us!
  109. 109. @michellesanver #phpfwdays Writing Good Code
  110. 110. @michellesanver #phpfwdays This talk is for you Questions? Contributions?
  111. 111. @michellesanver #phpfwdays Thank You #phpfwdays
  112. 112. Thank You #phpfwdays Michelle Sanver Liip

×