Slide 1

Slide 1 text

Better Living Through Events order.complete sendEmail() updateReport() drinkBeer() July 9, 2013 @ Nashville PHP

Slide 2

Slide 2 text

@epochblue

Slide 3

Slide 3 text

why are we here?

Slide 4

Slide 4 text

Let’s talk about... hot chicken

Slide 5

Slide 5 text

a hot chicken ordering system Input order Order Heard Order Started Order Checked Order Finished Order Up Order delivered

Slide 6

Slide 6 text

a hot chicken ordering system Input order updateOrder() updateOrder() updateOrder() updateOrder() updateOrder() Order delivered

Slide 7

Slide 7 text

sounds simple enough, right? function updateOrder() { $status = $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); break; // ... repeat for other states } }

Slide 8

Slide 8 text

the marketing department appears

Slide 9

Slide 9 text

email order updates function updateOrder() { $status = $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); break; // ... repeat for other states } $mailer = new Mailer( $this->getCustomer()->getEmail(), '[email protected]', "Your order is now ${this->getStatus()}!" ); $mailer->send(); }

Slide 10

Slide 10 text

more ideas appear

Slide 11

Slide 11 text

and they want metrics function updateOrder() { $status = $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); $db = $this->getReportingDB(); $db->timeToHeard( $this->getHeardTime() - $this->getCreatedTime() ); break; // ... repeat for other states } $mailer = new Mailer( $this->getCustomer()->getEmail(), '[email protected]', "Your order is now ${this->getStatus()}!" ); $mailer->send(); }

Slide 12

Slide 12 text

even more ideas appear Users should receive text messages as the state of their order changes Update a progress bar (Hi Josh!) on an UI somewhere on the line Every 10th order, the customer should be sent a satisfaction survey Every 50th order, send the customer a coupon for And think of all the metrics: Count orders that fail quality check Count orders that use coupons and how much they paid Send email for orders taking longer than 15 minutes to fill etc etc etc

Slide 13

Slide 13 text

AND before long YOU HAVE... function updateOrder() { $status = $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('ACCEPTED')->save(); $report = $this->getReportingDB(); $db->timeToAccepted( $this->getAcceptedTime() - $this->getCreatedTime() ); break; // ... repeat for other states case 'HEARD': $this->setCheckedTime(new \DateTime()); $this->setStatus('STARTED')->save(); $report = $this->getReportingDB(); $db->timeToStarted( $this->getStartedTime() - $this->getHeardTime() ); break; case 'STARTED': $this->setCompletedTime(new \DateTime()); $this->setStatus('COMPLETED')->save(); $report = $this->getReportingDB(); $db->timeToChecked( $this->getCheckedTime() - $this->getStartedTime() ); break; case 'COMPLETED': $this->setUpTime(new \DateTime()); $this->setStatus('UP')->save(); $report = $this->getReportingDB(); $db->timeTothisUp( $this->getUpTime() - $this->getCompletedTime() ); break; case 'UP': $this->setDeliveredTime(new \DateTime()); $this->setStatus('DELIVERED')->save(); $report = $this->getReportingDB(); $db->timeToDelivered( $this->getDeliveredTime() - $this->getUpTime() ); break; } $mailer = new Mailer( $this->getCustomer()->getEmail(), '[email protected]', "Your order is now ${this->getStatus()}!" ); $mailer->send(); } lolwut

Slide 14

Slide 14 text

what do we have? We have an Order object... ...that needs to update as the order progresses... ...and when it updates, lots of things need to happen. we need EVENTS

Slide 15

Slide 15 text

so what is an event? An event is an action detected by the system, whose occurrence is announced (the event is “fired”), and anything listening for the announcement performs some action (or actions) in response to the announcement (the event is “handled”). Huh?

Slide 16

Slide 16 text

again, with pictures order.complete Event announcement doSomething() doSomething() doSomething() do something

Slide 17

Slide 17 text

is there a pattern for this? yes! (two, in fact)

Slide 18

Slide 18 text

observer pattern Two parts: Subject & Observer Simple interface: Observers “subscribe” to Subjects Subjects “notify” Observers of changes Observers can “unsubscribe” from Subjects if they no longer want updates subject observer subject observer attach() subject observer update() subject observer detach()

Slide 19

Slide 19 text

mediator pattern Three parts: Subject, Observer, Mediator How it differs from Observer: Observers and Subjects communicate through the Mediator Has a similar interface: Observers “subscribe” to events thrown by the Mediator Subjects tell the Mediator to “publish” an event The Mediator “notifies” the subscribing Observers of the event subject observer mediator attach() update() publish()

Slide 20

Slide 20 text

back to the hot chicken.

Slide 21

Slide 21 text

what do we have now? We have an Order object... ...that needs to update as the order progresses... ...and when it updates, things need to happen... ...and now we need to change it to use events.

Slide 22

Slide 22 text

splsubject and splobserver

Slide 23

Slide 23 text

splsubject and splobserver interface SplSubject { public attach(\SplObserver $observer) public detach(\SplObserver $observer) public notify() } interface SplObserver { public update(\SplSubject $subject) }

Slide 24

Slide 24 text

Step 0: what we have now

Slide 25

Slide 25 text

where are we now? Order email update reporting DB

Slide 26

Slide 26 text

Step 1: order becomes a subject

Slide 27

Slide 27 text

order: now AS A subject class Order implements \SplSubject { private $observers = new \SplObjectStorage(); function attach(\SplObserver $observer) { $this->observers->attach($observer); } function detach(\SplObserver $observer) { $this->observers->detach($observer); } function notify() { foreach($this->observers as $o) { $o->update($this); } } // ...snip }

Slide 28

Slide 28 text

Pst. it looks the same. Order email update reporting DB

Slide 29

Slide 29 text

Step 2: email and report observers

Slide 30

Slide 30 text

EMAIL: NOW AS AN OBSERVER // EmailNotifier.php class EmailNotifier implements \SplObserver { public function update(\SplSubject $subject) { $mailer = new Mailer( $subject->getCustomer()->getEmail(), '[email protected]', "Your order is now ${subject->getStatus()}" ); $mailer->send(); } }

Slide 31

Slide 31 text

reporting: NOW AS AN OBSERVER // OrderReporter.php class OrderReporter implements \SplObserver { public function update(\SplSubject $subject) { $status = $subject->getStatus(); $db = $this->getReportingDB(); switch($status) { case 'NEW': $db->timeToHeard( $subject->getHeardTime() - $subject->getCreatedTime() ); break; // ... repeat for other states }

Slide 32

Slide 32 text

step 2 EmailNotifier OrderReporter Order email update reporting DB

Slide 33

Slide 33 text

Step 3: wire ‘em up

Slide 34

Slide 34 text

attach those observers // bootstrap.php $order = new Order(); $mail_observer = new EmailNotifier(...); $report_observer = new OrderReporter(...); $order->attach($mail_observer); $order->attach($report_observer);

Slide 35

Slide 35 text

step 3 EmailNotifier OrderReporter Order email update reporting DB

Slide 36

Slide 36 text

Step 4: simplify order updating

Slide 37

Slide 37 text

updateorder(): before function updateOrder() { $status = $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); $report = $this->getReportingDB(); $db->timeToAccepted( $this->getHeardTime() - $this->getCreatedTime() ); break; // ... repeat for other states } $mailer = new Mailer( $this->getCustomer()->getEmail(), '[email protected]', "Your order is now ${this->getStatus()}!" ); $mailer->send(); }

Slide 38

Slide 38 text

updateorder(): after function updateOrder() { $status = $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); break; // ... repeat for other states } $this->notify($this); }

Slide 39

Slide 39 text

side by side function updateOrder() { $status = $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('ACCEPTED')->save(); $report = $this->getReportingDB(); $db->timeToAccepted( $this->getAcceptedTime() - $this->getCreatedTime() ); break; // ... repeat for other states case 'HEARD': $this->setCheckedTime(new \DateTime()); $this->setStatus('STARTED')->save(); $report = $this->getReportingDB(); $db->timeToStarted( $this->getStartedTime() - $this->getHeardTime() ); break; case 'STARTED': $this->setCompletedTime(new \DateTime()); $this->setStatus('COMPLETED')->save(); $report = $this->getReportingDB(); $db->timeToChecked( $this->getCheckedTime() - $this->getStartedTime() ); break; case 'COMPLETED': $this->setUpTime(new \DateTime()); $this->setStatus('UP')->save(); $report = $this->getReportingDB(); $db->timeTothisUp( $this->getUpTime() - $this->getCompletedTime() ); break; case 'UP': $this->setDeliveredTime(new \DateTime()); $this->setStatus('DELIVERED')->save(); $report = $this->getReportingDB(); $db->timeToDelivered( $this->getDeliveredTime() - $this->getUpTime() ); break; } $mailer = new Mailer( $this->getCustomer()->getEmail(), '[email protected]', "Your order is now ${this->getStatus()}!" ); $mailer->send(); } function updateOrder() { $status = $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('ACCEPTED')->save(); break; case 'HEARD': $this->setCheckedTime(new \DateTime()); $this->setStatus('STARTED')->save(); break; case 'STARTED': $this->setCompletedTime(new \DateTime()); $this->setStatus('COMPLETED')->save(); break; case 'COMPLETED': $this->setUpTime(new \DateTime()); $this->setStatus('UP')->save(); break; case 'UP': $this->setDeliveredTime(new \DateTime()); $this->setStatus('DELIVERED')->save(); break; } $this->notify($this); }

Slide 40

Slide 40 text

and finally... Order EmailNotifier OrderReporter update()

Slide 41

Slide 41 text

so what do we have now? Email notifications and the reporting DB updates are no longer coupled to the Order object Other objects now have a mechanism for telling an Order that they want to know when it updates Order now has a mechanism for telling other objects when it updates And neither need to know that the other exists Using events here... Made our order update code cleaner, more focused, better encapsulated Decoupled our objects from each other

Slide 42

Slide 42 text

oh no! HERE COMES SCOPE CREEP!

Slide 43

Slide 43 text

“customer loyalty program” // FreeChicken.php class FreeChicken implements \SplObserver { public function update(\SplSubject $subject) { if ($subject->getStatus !== 'COMPLETED') { return; } $customer = $subject->getCustomer(); $orders = count($customer->getOrders()); if ($orders > 0 && $orders % 50 === 0)) { $customer->sendCouponForFreeChicken(); } } }

Slide 44

Slide 44 text

“customer loyalty program”, cont’d // bootstrap.php $order = new Order(); $mail_observer = new EmailNotifier(...); $report_observer = new OrderReporter(...); $free_chicken = new FreeChicken(...); $order->attach($mail_observer); $order->attach($report_observer); $order->attach($free_chicken);

Slide 45

Slide 45 text

let’s try again... Email notifications and the reporting DB updates are no longer coupled to the Order object Other objects now have a mechanism for telling an Order that they want to know when it updates Order now has a mechanism for telling other objects when it updates And neither needs to know that the other exists Using events here... Made our order update code cleaner, more focused, better encapsulated Decoupled our objects from each other Made changes/additions easier

Slide 46

Slide 46 text

observer drawbacks?

Slide 47

Slide 47 text

what about A mediator?

Slide 48

Slide 48 text

no more subject // Mediator.php class Mediator { private $events = array(); public function attach($event, $observer) { if (isset($this->events[$event])) { $this->events[$event]->attach($observer); } else { $this->events[$event] = new \SplObjectStorage(); $this->events[$event]->attach($observer); } } // ... wait for it

Slide 49

Slide 49 text

no more subject, CONT’d // ... wait for it public function detach($event, $observer) { if (isset($this->events[$event])) { $this->events[$event]->detach($observer); } } public function publish($event, $subject) { if (isset($this->events[$event])) { foreach($this->events[$event] as $ob) { $ob->update($subject); } } } }

Slide 50

Slide 50 text

so what happened to the subject?

Slide 51

Slide 51 text

see, there’s still a subject class Order { function updateOrder($order) { $status = $order->getStatus(); switch($status) { case 'NEW': $order->setAcceptedTime(new \DateTime()); $order->setStatus('ACCEPTED')->save(); break; // ...repeat for other states } $this->get('mediator')->publish('order.update', $this); } }

Slide 52

Slide 52 text

what about the observers? actually, they stay the same.

Slide 53

Slide 53 text

BUT HOOKing them UP IS DIFFERENT?

Slide 54

Slide 54 text

mediator hook ups // bootstrap.php $dispatcher = new Mediator(); $mail_observer = new EmailNotifier( ... ); $report_observer = new OrderReporter( ... ); $free_chicken = new FreeChicken( ... ); $dispatcher->attach('order.update', $mail_observer); $dispatcher->attach('order.update', $report_observer); $dispatcher->attach('order.update', $free_chicken);

Slide 55

Slide 55 text

OK. that was a lot. let’s recap.

Slide 56

Slide 56 text

once more, with feeling Events help decouple and clean up code when you have many (or an unknown number) of objects that need to know when the state of an object changes. Think: UI code, state machines, saving objects, beginning/ending a process... Observer Pattern Simpler, but more limited Better for small numbers of objects, persistent object, or singletons SplSubject/SplObserver Mediator Pattern (Slightly) More Complicated, but more flexible Typically better for web applications Symfony’s EventDispatcher component

Slide 57

Slide 57 text

MORE FUN WITH events

Slide 58

Slide 58 text

more with events Task/Message queues Event Loops IRC Bots Web servers NodeJS (see also: ReactPHP) Event-Driven Architecture

Slide 59

Slide 59 text

That’s it. Questions?

Slide 60

Slide 60 text

THanks! twitter & github: @epochblue

Slide 61

Slide 61 text

errata http://www.hattieb.com/ http://iostudio.com/ http://studionow.com/ http://en.wikipedia.org/wiki/Observer_pattern http://en.wikipedia.org/wiki/Mediator_pattern http://www.php.net/manual/en/class.splsubject.php http://www.php.net/manual/en/class.splobserver.php http://symfony.com/doc/current/components/event_dispatcher/introduction.html http://reactphp.org/ https://github.com/epochblue/philip/