Slide 1

Slide 1 text

@michellesanver #phpday Using the Workflow component for e-commerce
 #phpday | 10th of May 2019

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

@michellesanver #phpday Writing Good Code

Slide 5

Slide 5 text

@michellesanver #phpday This talk assumes little knowledge

Slide 6

Slide 6 text

@michellesanver #phpday This talk is… Open Source

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

@michellesanver #phpday Michelle Sanver Colour and code addict

Slide 9

Slide 9 text

@michellesanver #phpday Accent!?

Slide 10

Slide 10 text

@michellesanver #phpday

Slide 11

Slide 11 text

@michellesanver #phpday “It’s like Uber… 
 For shopping”

Slide 12

Slide 12 text

@michellesanver #phpday Order API Challenges

Slide 13

Slide 13 text

@michellesanver #phpday Keeping orders/prices up to date

Slide 14

Slide 14 text

@michellesanver #phpday Making our consumers love us
 (Good error messages and documentation)

Slide 15

Slide 15 text

@michellesanver #phpday Handling cancellation of an order

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

@michellesanver #phpday

Slide 18

Slide 18 text

@michellesanver #phpday State Machines

Slide 19

Slide 19 text

@michellesanver #phpday Why?

Slide 20

Slide 20 text

@michellesanver #phpday GREEN YELLOW RED

Slide 21

Slide 21 text

@michellesanver #phpday GREEN YELLOW RED

Slide 22

Slide 22 text

@michellesanver #phpday GREEN YELLOW RED

Slide 23

Slide 23 text

@michellesanver #phpday GREEN YELLOW RED

Slide 24

Slide 24 text

@michellesanver #phpday GREEN YELLOW RED Timer

Slide 25

Slide 25 text

@michellesanver #phpday GREEN YELLOW RED Timer T1

Slide 26

Slide 26 text

@michellesanver #phpday GREEN YELLOW RED Timer T1 T1

Slide 27

Slide 27 text

@michellesanver #phpday GREEN YELLOW RED Timer T1 T1 T1

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

@michellesanver #phpday GREEN RED ;) BLUE

Slide 31

Slide 31 text

@michellesanver #phpday GREEN YELLOW BLUE T1 T2 Workflow

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

@michellesanver #phpday We already think in states and workflows

Slide 34

Slide 34 text

@michellesanver #phpday The Order
 State Machine

Slide 35

Slide 35 text

@michellesanver #phpday Happy Workflow

Slide 36

Slide 36 text

@michellesanver #phpday 1. An order is created Open

Slide 37

Slide 37 text

@michellesanver #phpday 2. Items are updated Open

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

@michellesanver #phpday 4. User checks out Confirmed

Slide 40

Slide 40 text

@michellesanver #phpday 5. Pickers get notified Confirmed

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

@michellesanver #phpday 7. Picker goes shopping Picked Up

Slide 43

Slide 43 text

@michellesanver #phpday 8. Picker delivers order Delivered

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

@michellesanver #phpday Cancelled Aborted

Slide 47

Slide 47 text

@michellesanver #phpday Cancelled By System

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

@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

Slide 51

Slide 51 text

@michellesanver #phpday Open Open UpdateItem

Slide 52

Slide 52 text

@michellesanver #phpday State machines make business logic explicit

Slide 53

Slide 53 text

@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

Slide 54

Slide 54 text

@michellesanver #phpday A workflow does not contain all logic

Slide 55

Slide 55 text

@michellesanver #phpday The Workflow Component

Slide 56

Slide 56 text

@michellesanver #phpday composer require symfony/workflow

Slide 57

Slide 57 text

@michellesanver #phpday Our Workflow

Slide 58

Slide 58 text

@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

Slide 59

Slide 59 text

@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

Slide 60

Slide 60 text

@michellesanver #phpday framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundle\Entity\Order

Slide 61

Slide 61 text

@michellesanver #phpday framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundle\Entity\Order

Slide 62

Slide 62 text

@michellesanver #phpday framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundle\Entity\Order

Slide 63

Slide 63 text

@michellesanver #phpday framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundle\Entity\Order

Slide 64

Slide 64 text

@michellesanver #phpday framework: workflows: order: type: state_machine marking_store: type: 'single_state' arguments: - 'state' supports: - AppBundle\Entity\Order

Slide 65

Slide 65 text

@michellesanver #phpday places: - open - confirmed - assigned-to-picker - picked-up - delivered - aborted - cancelled - cancelled-by-system

Slide 66

Slide 66 text

@michellesanver #phpday transitions: updateItem: from: open to: open

Slide 67

Slide 67 text

@michellesanver #phpday The Model

Slide 68

Slide 68 text

@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';

Slide 69

Slide 69 text

@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, ];

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

@michellesanver #phpday

Slide 73

Slide 73 text

@michellesanver #phpday Events

Slide 74

Slide 74 text

@michellesanver #phpday Solves Tight Coupling

Slide 75

Slide 75 text

@michellesanver #phpday EventDispatcher

Slide 76

Slide 76 text

@michellesanver #phpday composer require symfony/event-dispatcher

Slide 77

Slide 77 text

@michellesanver #phpday The Listener

Slide 78

Slide 78 text

@michellesanver #phpday class OrderWorkflowListener implements EventSubscriberInterface {
 … }

Slide 79

Slide 79 text

@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', ]; }

Slide 80

Slide 80 text

@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', ]; }

Slide 81

Slide 81 text

@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', ]; }

Slide 82

Slide 82 text

@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', ]; }

Slide 83

Slide 83 text

@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', ]; }

Slide 84

Slide 84 text

@michellesanver #phpday Workflow component does so much for us

Slide 85

Slide 85 text

@michellesanver #phpday Order API Challenges Solved

Slide 86

Slide 86 text

@michellesanver #phpday Keeping orders/prices up to date

Slide 87

Slide 87 text

@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); }

Slide 88

Slide 88 text

@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); }

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

@michellesanver #phpday Making our consumers love us
 (Good error messages and documentation)

Slide 91

Slide 91 text

@michellesanver #phpday Handling cancellation of an order

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

@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); }

Slide 94

Slide 94 text

@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); }

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

@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; } } }

Slide 97

Slide 97 text

@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; } } }

Slide 98

Slide 98 text

@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, ];

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

@michellesanver #phpday Conclusion

Slide 101

Slide 101 text

@michellesanver #phpday We already think in states and workflows

Slide 102

Slide 102 text

@michellesanver #phpday State machines make business logic explicit

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

@michellesanver #phpday Writing Good Code

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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