Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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 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
  2. @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
  3. @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
  4. @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
  5. @michellesanver #phpday places: - open - confirmed - assigned-to-picker -

    picked-up - delivered - aborted - cancelled - cancelled-by-system
  6. @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';
  7. @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, ];
  8. @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', ]; }
  9. @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', ]; }
  10. @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', ]; }
  11. @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', ]; }
  12. @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', ]; }
  13. @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); }
  14. @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); }
  15. @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); }
  16. @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); }
  17. @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; } } }
  18. @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; } } }
  19. @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, ];